org.bibsonomy.model.util.BibTexUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.bibsonomy.model.util.BibTexUtils.java

Source

/**
 *
 *  BibSonomy-Model - Java- and JAXB-Model.
 *
 *  Copyright (C) 2006 - 2011 Knowledge & Data Engineering Group,
 *                            University of Kassel, Germany
 *                            http://www.kde.cs.uni-kassel.de/
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public License
 *  as published by the Free Software Foundation; either version 2
 *  of the License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package org.bibsonomy.model.util;

import static org.bibsonomy.util.ValidationUtils.present;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bibsonomy.common.enums.SerializeBibtexMode;
import org.bibsonomy.common.enums.SortKey;
import org.bibsonomy.common.enums.SortOrder;
import org.bibsonomy.model.BibTex;
import org.bibsonomy.model.PersonName;
import org.bibsonomy.model.Post;
import org.bibsonomy.model.comparators.BibTexPostComparator;
import org.bibsonomy.model.comparators.BibTexPostInterhashComparator;
import org.bibsonomy.services.URLGenerator;
import org.bibsonomy.util.StringUtils;
import org.bibsonomy.util.tex.TexDecode;

/**
 * Some BibTex utility functions.
 * 
 * @author Dominik Benz
 * @version $Id: BibTexUtils.java,v 1.83 2011-07-15 12:31:28 rja Exp $
 */
public class BibTexUtils {
    private static final Log log = LogFactory.getLog(BibTexUtils.class);

    /**
     * This field from the post is added to the BibTeX string (in addition to 
     * all fields from the resource) 
     */
    public static final String ADDITIONAL_MISC_FIELD_BIBURL = "biburl";

    /**
     * This field from the post is added to the BibTeX string (in addition to 
     * all fields from the resource) 
     */
    public static final String ADDITIONAL_MISC_FIELD_DESCRIPTION = "description";

    /**
     * This field from the post is added to the BibTeX string (in addition to 
     * all fields from the resource). It is needed by the DBLP update to allow
     * setting of the post date.
     */
    public static final String ADDITIONAL_MISC_FIELD_DATE = "date";

    /**
     * This field from the post is added to the BibTeX string (in addition to 
     * all fields from the resource). It represents the "date" field of the post.
     */
    public static final String ADDITIONAL_MISC_FIELD_ADDED_AT = "added-at";
    /**
     * This field from the post is added to the BibTeX string (in addition to 
     * all fields from the resource). It represents the "changeDate" of the post. 
     */
    public static final String ADDITIONAL_MISC_FIELD_TIMESTAMP = "timestamp";

    /**
     * ISO date + time for "added-at" and "timestamp" field  
     */
    private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

    /**
     * This field from the post is added to the BibTeX string (in addition to 
     * all fields from the resource)
     */
    public static final String ADDITIONAL_MISC_FIELD_KEYWORDS = "keywords";

    /**
     * This field from the post is added to the BibTeX string (in addition to 
     * all fields from the resource)
     */
    public static final String ADDITIONAL_MISC_FIELD_PRIVNOTE = "privnote";

    /**
     * This fields from the post are added to the BibTeX string (in addition to 
     * all fields from the resource)
     */
    public static final String[] ADDITIONAL_MISC_FIELDS = new String[] { ADDITIONAL_MISC_FIELD_DESCRIPTION,
            ADDITIONAL_MISC_FIELD_KEYWORDS, ADDITIONAL_MISC_FIELD_BIBURL, ADDITIONAL_MISC_FIELD_PRIVNOTE,
            ADDITIONAL_MISC_FIELD_ADDED_AT, ADDITIONAL_MISC_FIELD_TIMESTAMP };

    /**
     * the supported entrytypes of a bibtex
     * be careful when changing order some code uses the order to map entrytypes to (swrc|ris) entrytypes
     * 
     * e.g., in org.bibsonomy.model.util.BibTexUtils.ENTRYTYPES 
     * 
     * FIXME: this is bad. Please fix this behaviour. 
     */
    public static final String[] ENTRYTYPES = { "article", "book", "booklet", "conference", "electronic", "inbook",
            "incollection", "inproceedings", "manual", "mastersthesis", "misc", "patent", "periodical", "phdthesis",
            "preamble", "presentation", "proceedings", "standard", "techreport", "unpublished" };

    /*
     * patterns used for matching
     */
    private static final Pattern YEAR_PATTERN = Pattern.compile("\\d{4}");
    private static final Pattern DOI_PATTERN = Pattern.compile("http://.+/(.+?/.+?$)");
    private static final Pattern LAST_COMMA_PATTERN = Pattern.compile(".+}?\\s*,\\s*}\\s*$",
            Pattern.MULTILINE | Pattern.DOTALL);
    private static final Pattern NUMERIC_PATTERN = Pattern.compile("^\\d+$");

    /*
     * fields to be excluded when creating bibtex strings.
     */
    private static final Set<String> EXCLUDE_FIELDS = new HashSet<String>(Arrays.asList(new String[] { "abstract", // added separately
            "bibtexAbstract", // added separately
            "bibtexKey", // added at beginning of entry
            "entrytype", // added at beginning of entry
            "misc", // contains several fields; handled separately 
            "month", // handled separately
            "openURL", "simHash0", // not added
            "simHash1", // not added
            "simHash2", // not added
            "simHash3" // not added
    }));

    /**
     * Some BibTeX styles translate month abbreviations into (language specific) 
     * month names. If we find such a month abbreviation, we should not put 
     * braces around the string.
     */
    private static final Map<String, Integer> BIBTEX_MONTHS = new HashMap<String, Integer>();
    static {
        final String[] months = new String[] { "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct",
                "nov", "dec" };
        for (int i = 0; i < months.length; i++) {
            BIBTEX_MONTHS.put(months[i], i + 1);
        }
    }

    /** default opening bracket */
    public static final char DEFAULT_OPENING_BRACKET = '{';
    /** default closing bracket */
    public static final char DEFAULT_CLOSING_BRACKET = '}';
    /** value separator used to separate key/value pairs; i.e. key=val SEP key2=val2*/
    public static final char KEYVALUE_SEPARATOR = ',';
    /** assignment operator to assign keys to values; i.e. key OP val, ...*/
    public static final char ASSIGNMENT_OPERATOR = '=';
    /** indentation used for key/value pairs when converted to a bibtex string */
    public static final String KEYVALUE_INDENT = "  ";

    /**
     * Builds a string from a given bibtex object which can be used to build an OpenURL
     * see http://www.exlibrisgroup.com/sfx_openurl.htm
     *
     * @param bib the bibtex object
     * @return the DESCRIPTION part of the OpenURL of this BibTeX object
     */
    public static String getOpenurl(final BibTex bib) {
        // stores the completed URL (just the DESCRIPTION part)
        final StringBuilder openurl = new StringBuilder();

        /*
         * extract first authors parts of the name
         */
        // get first author (if author not present, use editor)
        String author = bib.getAuthor();
        if (!present(author)) {
            author = bib.getEditor();
        }
        // TODO: this is only necessary because of broken (DBLP) entries which have neither author nor editor!
        if (!present(author)) {
            author = "";
        }
        final PersonName personName = new PersonName(
                author.replaceFirst(PersonNameUtils.PERSON_NAME_DELIMITER + ".*", "").trim());
        // check, if first name is just an initial
        final String auinit1;
        final String firstName = personName.getFirstName();
        if (present(firstName) && firstName.length() == 1) {
            auinit1 = firstName;
            personName.setFirstName(null);
        } else {
            auinit1 = null;
        }

        // parse misc fields
        bib.parseMiscField();
        // extract DOI
        String doi = bib.getMiscField("doi");
        if (doi != null) {
            // TODO: urls rausfiltern testen
            final Matcher m = DOI_PATTERN.matcher(doi);
            if (m.find()) {
                doi = m.group(1);
            }
        }

        try {
            // append year (due to inconsistent database not always given!)
            if (present(bib.getYear())) {
                openurl.append("date=" + bib.getYear().trim());
            }
            // append doi
            if (present(doi)) {
                appendOpenURL(openurl, "id", "doi:" + doi.trim());
            }
            // append isbn + issn
            appendOpenURL(openurl, "isbn", bib.getMiscField("isbn"));
            appendOpenURL(openurl, "issn", bib.getMiscField("issn"));
            // append name information for first author
            appendOpenURL(openurl, "aulast", personName.getLastName());
            appendOpenURL(openurl, "aufirst", firstName);
            appendOpenURL(openurl, "auinit1", auinit1);
            // genres == entrytypes
            if (bib.getEntrytype().toLowerCase().equals("journal")) {
                appendOpenURL(openurl, "genre", "journal");
                appendOpenURL(openurl, "title", bib.getTitle());
            } else if (bib.getEntrytype().toLowerCase().equals("book")) {
                appendOpenURL(openurl, "genre", "book");
                appendOpenURL(openurl, "title", bib.getTitle());
            } else if (bib.getEntrytype().toLowerCase().equals("article")) {
                appendOpenURL(openurl, "genre", "article");
                appendOpenURL(openurl, "title", bib.getJournal());
                appendOpenURL(openurl, "atitle", bib.getTitle());
            } else if (bib.getEntrytype().toLowerCase().equals("inbook")) {
                appendOpenURL(openurl, "genre", "bookitem");
                appendOpenURL(openurl, "title", bib.getBooktitle());
                appendOpenURL(openurl, "atitle", bib.getTitle());
            } else if (bib.getEntrytype().toLowerCase().equals("proceedings")) {
                appendOpenURL(openurl, "genre", "proceeding");
                appendOpenURL(openurl, "title", bib.getBooktitle());
                appendOpenURL(openurl, "atitle", bib.getTitle());
            } else {
                appendOpenURL(openurl, "title", bib.getBooktitle());
                appendOpenURL(openurl, "atitle", bib.getTitle());
            }
            appendOpenURL(openurl, "volume", bib.getVolume());
            appendOpenURL(openurl, "issue", bib.getNumber());
        } catch (final UnsupportedEncodingException ex) {
            log.error("error while generating openURL", ex);
        }

        return openurl.toString();
    }

    private static void appendOpenURL(final StringBuilder buffer, final String name, final String value)
            throws UnsupportedEncodingException {
        if (value != null && !value.trim().equals("")) {
            buffer.append("&" + name + "=" + URLEncoder.encode(value.trim(), "UTF-8"));
        }
    }

    /**
     * return a bibtex string representation of the given bibtex object. by default, 
     * the contained misc fields are parsed before the bibtex string is generated.
     * 
     * @param bib - the bibtex object
     * @return - a string representation of the given bibtex object
     */
    public static String toBibtexString(final BibTex bib) {
        return toBibtexString(bib, SerializeBibtexMode.PARSED_MISCFIELDS);
    }

    /**
     * return a bibtex string representation of the given bibtex object
     * 
     * @param bib - a bibtex object
     * @param mode - the serializing mode (parse misc fields or include misc fields as they are)
     * @return String bibtexString
     * 
     * TODO use BibTex.DEFAULT_OPENBRACKET etc.
     * 
     */
    public static String toBibtexString(final BibTex bib, SerializeBibtexMode mode) {
        try {
            final BeanInfo bi = Introspector.getBeanInfo(bib.getClass());

            /*
             * start with entrytype and key
             */
            final StringBuilder buffer = new StringBuilder(
                    "@" + bib.getEntrytype() + "{" + bib.getBibtexKey() + ",\n");

            /*
             * append all other fields
             */
            for (final PropertyDescriptor d : bi.getPropertyDescriptors()) {
                final Method getter = d.getReadMethod();
                // loop over all String attributes
                final Object o = getter.invoke(bib, (Object[]) null);
                if (String.class.equals(d.getPropertyType()) && o != null
                        && !EXCLUDE_FIELDS.contains(d.getName())) {

                    /*
                     * Strings containing whitespace give empty fields ... we ignore them 
                     */
                    String value = ((String) o);
                    if (present(value)) {
                        if (!NUMERIC_PATTERN.matcher(value).matches()) {
                            value = DEFAULT_OPENING_BRACKET + value + DEFAULT_CLOSING_BRACKET;
                        }
                        buffer.append("  " + d.getName().toLowerCase() + " = " + value + ",\n");
                    }
                }
            }
            /*
             * process miscFields map, if present
             */
            if (present(bib.getMiscFields())) {
                if (mode.equals(SerializeBibtexMode.PARSED_MISCFIELDS) && !bib.isMiscFieldParsed()) {
                    // parse misc field, if not yet done
                    bib.parseMiscField();
                }
                buffer.append(serializeMiscFields(bib.getMiscFields(), true));
            }

            /*
             * include plain misc fields if desired
             */
            if (mode.equals(SerializeBibtexMode.PLAIN_MISCFIELDS) && present(bib.getMisc())) {
                buffer.append("  " + bib.getMisc() + ",\n");
            }
            /*
             * add month
             */
            final String month = bib.getMonth();
            if (present(month)) {
                // we don't add {}, this is done by getMonth(), if necessary
                buffer.append("  month = " + getMonth(month) + ",\n");
            }
            /*
             * add abstract
             */
            final String bibAbstract = bib.getAbstract();
            if (present(bibAbstract)) {
                buffer.append("  abstract = {" + bibAbstract + "},\n");
            }
            /*
             * remove last comma
             */
            buffer.delete(buffer.lastIndexOf(","), buffer.length());
            buffer.append("\n}");

            return buffer.toString();

        } catch (IntrospectionException ex) {
            ex.printStackTrace();
        } catch (InvocationTargetException ex) {
            ex.printStackTrace();
        } catch (IllegalAccessException ex) {
            ex.printStackTrace();
        }
        return null;
    }

    /**
     * Some BibTeX styles translate month abbreviations into (language specific) 
     * month names. If we find such a month abbreviation, we should not put 
     * braces around the string. This method returns the correct string - with
     * braces, if it's not an abbreviation, without otherwise.
     * 
     * @param month
     * @return The correctly 'quoted' month.
     */
    public static String getMonth(final String month) {
        if (month != null && BIBTEX_MONTHS.containsKey(month.toLowerCase().trim()))
            return month;
        return "{" + month + "}";
    }

    /**
     * Tries to extract the month number from the given string. The following 
     * input formats are supported:
     * <ul>
     * <li>long English month name: January, february, MARCH, ...</li>
     * <li>abbreviated English month name: Jan, feb, MAR, ...</li>
     * <li>month as number: 01, 2, 3, ...</li>
     * </ul> 
     * <strong>Note:</strong> if an unreadable month is given, the untouched
     * string is returned. 
     * 
     * 
     * @param month
     * @return The month represented as number in the range 1, ..., 12
     */
    public static String getMonthAsNumber(final String month) {
        if (present(month)) {
            final String trimmed = month.replace('#', ' ').trim();
            if (trimmed.length() >= 3) {
                final String abbrev = trimmed.toLowerCase().substring(0, 3);
                if (BIBTEX_MONTHS.containsKey(abbrev)) {
                    return BIBTEX_MONTHS.get(abbrev).toString();
                }
            }
            return trimmed;
        }
        return month;
    }

    /**
     * Creates a bibtex string with some bibsonomy-specific information using 
     * {@link #toBibtexString(Post)}.
     * 
     * <ul>
     *       <li>tags in <code>keywords</code> field</li>
     *       <li>URL to bibtex details page in <code>biburl</code> field</li>
     *       <li>description in the <code>description</code> field</li>
     * </ul>
     * 
     * @see #toBibtexString(Post)
     * 
     * @param post 
     *          - a bibtex post
     * @param urlGenerator - to generate a proper URL pointing to the post. 
     * 
     * @return A string representation of the posts in BibTeX format.
     */
    public static String toBibtexString(final Post<BibTex> post, final URLGenerator urlGenerator) {
        post.getResource().addMiscField(ADDITIONAL_MISC_FIELD_BIBURL,
                urlGenerator.getPublicationUrl(post.getResource(), post.getUser()).toString());
        return toBibtexString(post);
    }

    /**
     * Return a bibtex representation of the given post. Defaults to 
     * serialize mode PARSED_MISCFIELDS.
     * 
     * @param post - a post
     * @return - a bibtex string representation of this post.
     */
    public static String toBibtexString(final Post<BibTex> post) {
        return toBibtexString(post, SerializeBibtexMode.PARSED_MISCFIELDS);
    }

    /**
     * Creates a BibTeX string containing more than only the fields in the 
     * BibTeX object:
     * 
     * <ul>
     *       <li>tags in the <code>keywords</code> field</li>
     *      <li>description in the <code>description</code> field</li>
     * </ul>
     * 
     * @param post - a BibTeX post.
     * @param mode - the serialize mode
     * 
     * @return A string representation of the post in BibTeX format.
     */
    public static String toBibtexString(final Post<BibTex> post, SerializeBibtexMode mode) {
        final BibTex bib = post.getResource();
        /*
         * add additional fields.
         *  
         * ATTENTION: if you add fields here, you have to add them also 
         * (in SimpleBibTeXParser.updateWithParsedBibTeX!)
         * in ADDITIONAL_MISC_FIELDS. Thus when someone enters a bibtex field with the 
         * name of your added field, it will not be stored in the misc section.
         */
        bib.addMiscField(ADDITIONAL_MISC_FIELD_KEYWORDS, TagUtils.toTagString(post.getTags(), " "));
        if (present(post.getDescription())) {
            bib.addMiscField(ADDITIONAL_MISC_FIELD_DESCRIPTION, post.getDescription());
        }
        if (present(post.getDate())) {
            bib.addMiscField(ADDITIONAL_MISC_FIELD_ADDED_AT, DATE_FORMAT.format(post.getDate()));
        }
        if (present(post.getChangeDate())) {
            bib.addMiscField(ADDITIONAL_MISC_FIELD_TIMESTAMP, DATE_FORMAT.format(post.getDate()));
        }
        return toBibtexString(bib, mode);
    }

    /**
     * @see #generateBibtexKey(String, String, String, String)
     * @param bib
     * 
     * @return The generated BibTeX key.
     */
    public static String generateBibtexKey(final BibTex bib) {
        if (bib == null)
            return "";
        return generateBibtexKey(bib.getAuthor(), bib.getEditor(), bib.getYear(), bib.getTitle());
    }

    /**
     * Generates a bibtex key of the form "first persons lastname from authors
     * or editors" or "noauthororeditor" concatenated with year.
     * 
     * @param authors
     *            some string representation of the list of authors with their
     *            first- and lastnames
     * @param editors
     *            some string representation of the list of editors with their
     *            first- and lastnames
     * @param year
     * @param title
     * @return a bibtex key for a bibtex with the fieldvalues given by arguments
     */
    public static String generateBibtexKey(final String authors, final String editors, final String year,
            final String title) {
        /*
         * TODO: pick either author or editor. DON'T use getAuthorlist (it sorts alphabetically!). CHECK for null values.
         * What to do with Chinese authors and other broken names?
         * How to extract the first RELEVANT word of the title?
         * remove Sonderzeichen, LaTeX markup!
         */
        final StringBuilder buffer = new StringBuilder();

        /* get author */
        String first = PersonNameUtils.getFirstPersonsLastName(authors);
        if (first == null) {
            first = PersonNameUtils.getFirstPersonsLastName(editors);
            if (first == null) {
                first = "noauthororeditor";
            }
        }
        buffer.append(first);

        /* the year */
        if (year != null) {
            buffer.append(year.trim());
        }

        /* first relevant word of the title */
        if (title != null) {
            /* best guess: pick first word with more than 4 characters, longest first word */
            // FIXME: what do we want to do inside this if statement?
            buffer.append(getFirstRelevantWord(title).toLowerCase());
        }

        return buffer.toString().toLowerCase().replaceAll("[^a-zA-Z0-9]", "");
    }

    /**
     * Relevant = longer than four characters (= 0-9a-z)
     * 
     * @param title
     * @return
     */
    private static String getFirstRelevantWord(final String title) {
        final String[] split = title.split("\\s");
        for (final String s : split) {
            final String ss = s.replaceAll("[^a-zA-Z0-9]", "");
            if (ss.length() > 4) {
                return ss;
            }
        }
        return "";
    }

    /**
     * Cleans up a string containing LaTeX markup and converts special chars to HTML special chars.
     * 
     * @param bibtex a bibtex string
     * @return the cleaned bibtex string
     */
    public static String cleanBibTex(String bibtex) {

        if (!present(bibtex))
            return "";

        // replace markup
        bibtex = bibtex.replaceAll("\\\\[a-z]+\\{([^\\}]+)\\}", "$1"); // \\markup{marked_up_text}      

        // decode Latex macros into unicode characters
        bibtex = TexDecode.decode(bibtex).trim();

        // convert non-ASCII into HTML entities
        final StringBuilder buffer = new StringBuilder(bibtex.length());
        char c;
        for (int i = 0; i < bibtex.length(); i++) {
            c = bibtex.charAt(i);

            // HTML Special Chars
            if (c == '"')
                buffer.append("&quot;");
            else if (c == '&')
                buffer.append("&amp;");
            else if (c == '<')
                buffer.append("&lt;");
            else if (c == '>')
                buffer.append("&gt;");
            else {
                int ci = 0xffff & c;
                if (ci < 160)
                    // nothing special only 7 Bit
                    buffer.append(c);
                else {
                    // Not 7 Bit use the unicode system
                    buffer.append("&#");
                    buffer.append(new Integer(ci).toString());
                    buffer.append(';');
                }
            }
        }
        return buffer.toString();
    }

    /**
     * Tries to find a year (four connected digits) in a string and returns them as int.
     * If it fails, returns Integer.MAX_VALUE.
     * 
     * @param year
     * @return an integer representation of the year, or Integer.MAX_VALUE if it fails
     */
    public static int getYear(final String year) {
        try {
            return Integer.parseInt(year);
        } catch (final NumberFormatException ignore) {
            /*
             * try to get four digits ...
             */
            final Matcher m = YEAR_PATTERN.matcher(year);
            if (m.find()) {
                return Integer.parseInt(m.group());
            }
        }
        return Integer.MAX_VALUE;
    }

    /**
     * Sort a list of bibtex posts (and eventually remove duplicates).
     * 
     * @param bibtexList
     * @param sortKeys
     * @param sortOrders
     */
    public static void sortBibTexList(final List<Post<BibTex>> bibtexList, final List<SortKey> sortKeys,
            final List<SortOrder> sortOrders) {
        if (present(bibtexList) && bibtexList.size() > 1) {
            Collections.sort(bibtexList, new BibTexPostComparator(sortKeys, sortOrders));
        }
    }

    /**
     * Sorts a list of bibtex posts and removes duplicates.
     * 
     * @param bibtexList
     */
    public static void removeDuplicates(final List<Post<BibTex>> bibtexList) {
        final Set<Post<BibTex>> temp = new TreeSet<Post<BibTex>>(new BibTexPostInterhashComparator());
        temp.addAll(bibtexList);
        // FIXME: a bit cumbersome at this point - but we need to work on the bibtexList
        bibtexList.clear();
        bibtexList.addAll(temp);
    }

    /** Adds the field <code>fieldName</code> to the BibTeX entry, if the entry 
     * does not already contain it.
     * 
     * @param bibtex - the BibTeX entry
     * @param fieldName - the name of the field
     * @param fieldValue - the value of the field
     * 
     * @return The new BibTeX entry.
     */
    public static String addFieldIfNotContained(final String bibtex, final String fieldName,
            final String fieldValue) {
        if (bibtex == null)
            return bibtex;

        final StringBuffer buf = new StringBuffer(bibtex);
        addFieldIfNotContained(buf, fieldName, fieldValue);
        return buf.toString();
    }

    /** Adds the field <code>fieldName</code> to the BibTeX entry, if the entry 
     * does not already contain it.
     * 
     * @param bibtex - the BibTeX entry
     * @param fieldName - the name of the field
     * @param fieldValue - the value of the field
     * 
     */
    public static void addFieldIfNotContained(final StringBuffer bibtex, final String fieldName,
            final String fieldValue) {
        if (bibtex == null)
            return;
        /*
         * it seems, we can do regex stuff only on strings ... so we have 
         * to convert the buffer into a string :-(
         */
        final String bibtexString = bibtex.toString();
        /*
         * The only way safe to find out if the entry already contains
         * the field is to parse it. This is expensive, thus we only 
         * do simple heuristics, which is of course, error prone! 
         */
        if (!bibtexString.matches("(?s).*" + fieldName + "\\s*=\\s*.*")) {
            /*
             * add the field at the end before the last brace
             */
            addField(bibtex, fieldName, fieldValue);
        }
    }

    /** Adds the given field at the end of the given BibTeX entry by placing
     * it before the last brace. 
     * 
     * @param bibtex - the BibTeX entry
     * @param fieldName - the name of the field
     * @param fieldValue - the value of the field
     */
    public static void addField(final StringBuffer bibtex, final String fieldName, final String fieldValue) {
        /*
         * ignore empty bibtex and empty field values
         */
        if (bibtex == null || fieldValue == null || fieldValue.trim().equals(""))
            return;

        /*
         * remove last comma if there is one (before closing last curly bracket)
         */
        final String bib = bibtex.toString().trim();
        final Matcher m = LAST_COMMA_PATTERN.matcher(bib);

        if (m.matches()) {
            final int _lastIndex = bib.lastIndexOf(",");
            bibtex.replace(_lastIndex, _lastIndex + 1, "");
        }

        final int lastIndexOf = bibtex.lastIndexOf("}");
        if (lastIndexOf > 0) {
            bibtex.replace(lastIndexOf, bibtex.length(), "," + fieldName + " = {" + fieldValue + "}\n}");
        }
    }

    /**
     * replaces all " and "'s in author and editor with a new line
     * @param bibtex
     */
    public static void prepareEditorAndAuthorFieldForView(final BibTex bibtex) {
        final String author = prepareNameRepresentationForView(bibtex.getAuthor());
        final String editor = prepareNameRepresentationForView(bibtex.getEditor());

        bibtex.setAuthor(author);
        bibtex.setEditor(editor);
    }

    private static String prepareNameRepresentationForView(final String string) {
        return present(string) ? string.replaceAll(PersonNameUtils.PERSON_NAME_DELIMITER, "\n") : "";
    }

    /**
     * reverses {@link #prepareEditorAndAuthorFieldForView(BibTex)}
     * (replaces new line with an " and ")
     * 
     * @param bibtex
     */
    public static void prepareEditorAndAuthorFieldForDatabase(final BibTex bibtex) {
        final String author = prepareNameRepresentationForDatabase(bibtex.getAuthor());
        final String editor = prepareNameRepresentationForDatabase(bibtex.getEditor());

        bibtex.setAuthor(author);
        bibtex.setEditor(editor);
    }

    private static String prepareNameRepresentationForDatabase(final String string) {
        return present(string) ? string.replaceAll("\n", PersonNameUtils.PERSON_NAME_DELIMITER) : "";
    }

    /**
     * Converts the key = value pairs contained in the 
     * miscFields map of a bibtex object into a serialized representation in the 
     * misc-Field. It appends 
     * 
     *  key1 = {value1}, key2 = {value2}, ...
     *  
     * for all defined miscFields to the return string.
     * 
     * @param miscFields - a map containing key/value pairs
     * @param appendTrailingSeparator - whether to append a trailing separator at the end of the string
     * @return - a string representation of the given object.
     */
    public static String serializeMiscFields(Map<String, String> miscFields, boolean appendTrailingSeparator) {
        final StringBuilder miscFieldsSerialized = new StringBuilder();
        // loop over misc fields, if any
        if (present(miscFields)) {
            final Iterator<String> it = miscFields.keySet().iterator();
            while (it.hasNext()) {
                final String currKey = it.next();
                miscFieldsSerialized.append(KEYVALUE_INDENT + currKey.toLowerCase() + " " + ASSIGNMENT_OPERATOR
                        + " " + DEFAULT_OPENING_BRACKET + miscFields.get(currKey) + DEFAULT_CLOSING_BRACKET);
                if (it.hasNext() || appendTrailingSeparator) {
                    miscFieldsSerialized.append(KEYVALUE_SEPARATOR + "\n");
                }
            }

        }
        // write serialized misc fields into misc field
        return miscFieldsSerialized.toString();
    }

    /**
     * Parse a given misc field string into a hashmap containing key/value pairs.
     * 
     * @param miscFieldString - the misc field string
     * @return a hashmap containg the parsed key/value pairs.
     */
    public static Map<String, String> parseMiscFieldString(String miscFieldString) {
        return StringUtils.parseBracketedKeyValuePairs(miscFieldString, ASSIGNMENT_OPERATOR, KEYVALUE_SEPARATOR,
                DEFAULT_OPENING_BRACKET, DEFAULT_CLOSING_BRACKET);
    }

}