<commit_before>/*
Copyright (C) 2003 <NAME>, <NAME>

All programs in this directory and
subdirectories are published under the GNU General Public License as
described below.

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU 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
General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA

Further information about the GNU GPL is available at:
http://www.gnu.org/copyleft/gpl.ja.html

Note:
Modified for use in JabRef

*/
package net.sf.jabref.model;

import net.sf.jabref.*;
import net.sf.jabref.logic.l10n.Localization;
import net.sf.jabref.logic.util.MonthUtil;

import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class BibtexDatabase {

    private final Map<String, BibtexEntry> entries = new Hashtable<>();

    private String preamble;

    private final HashMap<String, BibtexString> bibtexStrings = new HashMap<>();

    private final Set<DatabaseChangeListener> changeListeners = new HashSet<>();

    private boolean followCrossrefs = true;

    private static final Log LOGGER = LogFactory.getLog(BibtexDatabase.class);

    /**
     * use a map instead of a set since i need to know how many of each key is
     * inthere
     */
    private final HashMap<String, Integer> allKeys = new HashMap<>();

    /*
     * Entries are stored in a HashMap with the ID as key. What happens if
     * someone changes a BibtexEntry's ID after it has been added to this
     * BibtexDatabase? The key of that entry would be the old ID, not the new
     * one. Use a PropertyChangeListener to identify an ID change and update the
     * Map.
     */
    private final VetoableChangeListener listener =
            propertyChangeEvent -> {
                if (propertyChangeEvent.getPropertyName() == null) {
                    fireDatabaseChanged(new DatabaseChangeEvent(BibtexDatabase.this, DatabaseChangeEvent.ChangeType.CHANGING_ENTRY, (BibtexEntry) propertyChangeEvent.getSource()));
                } else if ("id".equals(propertyChangeEvent.getPropertyName())) {
                    // locate the entry under its old key
                    BibtexEntry oldEntry =
                            entries.remove(propertyChangeEvent.getOldValue());

                    if (oldEntry != propertyChangeEvent.getSource()) {
                        // Something is very wrong!
                        // The entry under the old key isn't
                        // the one that sent this event.
                        // Restore the old state.
                        entries.put((String) propertyChangeEvent.getOldValue(), oldEntry);
                        throw new PropertyVetoException("Wrong old ID", propertyChangeEvent);
                    }

                    if (entries.get(propertyChangeEvent.getNewValue()) != null) {
                        entries.put((String) propertyChangeEvent.getOldValue(), oldEntry);
                        throw new PropertyVetoException
                                ("New ID already in use, please choose another",
                                        propertyChangeEvent);
                    }

                    // and re-file this entry
                    entries.put((String) propertyChangeEvent.getNewValue(),
                            (BibtexEntry) propertyChangeEvent.getSource());
                } else {
                    fireDatabaseChanged(new DatabaseChangeEvent(BibtexDatabase.this, DatabaseChangeEvent.ChangeType.CHANGED_ENTRY, (BibtexEntry) propertyChangeEvent.getSource()));
                }
            };


    /**
     * Returns the number of entries.
     */
    public synchronized int getEntryCount() {
        return entries.size();
    }

    /**
     * Returns a Set containing the keys to all entries.
     * Use getKeySet().iterator() to iterate over all entries.
     */
    public synchronized Set<String> getKeySet() {
        return entries.keySet();
    }

    /**
     * Returns an EntrySorter with the sorted entries from this base,
     * sorted by the given Comparator.
     */
    public synchronized EntrySorter getSorter(Comparator<BibtexEntry> comp) {
        EntrySorter sorter = new EntrySorter(entries, comp);
        addDatabaseChangeListener(sorter);
        return sorter;
    }

    /**
     * Just temporary, for testing purposes....
     *
     * @return
     */
    public Map<String, BibtexEntry> getEntryMap() {
        return entries;
    }

    /**
     * Returns the entry with the given ID (-> entry_type + hashcode).
     */
    public synchronized BibtexEntry getEntryById(String id) {
        return entries.get(id);
    }

    public synchronized Collection<BibtexEntry> getEntries() {
        return entries.values();
    }

    /**
     * Returns the entry with the given bibtex key.
     */
    public synchronized BibtexEntry getEntryByKey(String key) {
        BibtexEntry back = null;

        int keyHash = key.hashCode(); // key hash for better performance

        Set<String> keySet = entries.keySet();
        for (String entrieID : keySet) {
            BibtexEntry entry = getEntryById(entrieID);
            if (entry != null && entry.getCiteKey() != null) {
                String citeKey = entry.getCiteKey();
                if (citeKey != null) {
                    if (keyHash == citeKey.hashCode()) {
                        back = entry;
                    }
                }
            }
        }
        return back;
    }

    public synchronized BibtexEntry[] getEntriesByKey(String key) {

        ArrayList<BibtexEntry> entries = new ArrayList<BibtexEntry>();

        for (BibtexEntry entry : this.entries.values()) {
            if (key.equals(entry.getCiteKey())) {
                entries.add(entry);
            }
        }

        return entries.toArray(new BibtexEntry[entries.size()]);
    }

    /**
     * Inserts the entry, given that its ID is not already in use.
     * use Util.createId(...) to make up a unique ID for an entry.
     */
    public synchronized boolean insertEntry(BibtexEntry entry)
            throws KeyCollisionException {
        String id = entry.getId();
        if (getEntryById(id) != null) {
            throw new KeyCollisionException(
                    "ID is already in use, please choose another");
        }

        entry.addPropertyChangeListener(listener);

        entries.put(id, entry);

        fireDatabaseChanged(new DatabaseChangeEvent(this, DatabaseChangeEvent.ChangeType.ADDED_ENTRY, entry));

        return checkForDuplicateKeyAndAdd(null, entry.getCiteKey());
    }

    /**
     * Removes the entry with the given string.
     * <p>
     * Returns null if not found.
     */
    public synchronized BibtexEntry removeEntry(String id) {
        BibtexEntry oldValue = entries.remove(id);

        if (oldValue == null) {
            return null;
        }

        removeKeyFromSet(oldValue.getCiteKey());
        oldValue.removePropertyChangeListener(listener);
        fireDatabaseChanged(new DatabaseChangeEvent(this, DatabaseChangeEvent.ChangeType.REMOVED_ENTRY, oldValue));

        return oldValue;
    }

    public synchronized boolean setCiteKeyForEntry(String id, String key) {
        if (!entries.containsKey(id)) {
            return false; // Entry doesn't exist!
        }
        BibtexEntry entry = getEntryById(id);
        String oldKey = entry.getCiteKey();
        if (key != null) {
            entry.setField(BibtexFields.KEY_FIELD, key);
        } else {
            entry.clearField(BibtexFields.KEY_FIELD);
        }
        return checkForDuplicateKeyAndAdd(oldKey, entry.getCiteKey());
    }

    /**
     * Sets the database's preamble.
     */
    public synchronized void setPreamble(String preamble) {
        this.preamble = preamble;
    }

    /**
     * Returns the database's preamble.
     */
    public synchronized String getPreamble() {
        return preamble;
    }

    /**
     * Inserts a Bibtex String at the given index.
     */
    public synchronized void addString(BibtexString string)
            throws KeyCollisionException {
        if (hasStringLabel(string.getName())) {
            throw new KeyCollisionException(Localization.lang("A string with this label already exists"));
        }

        if (bibtexStrings.containsKey(string.getId())) {
            throw new KeyCollisionException("Duplicate BibtexString id.");
        }

        bibtexStrings.put(string.getId(), string);
    }

    /**
     * Removes the string at the given index.
     */
    public synchronized void removeString(String id) {
        bibtexStrings.remove(id);
    }

    /**
     * Returns a Set of keys to all BibtexString objects in the database.
     * These are in no sorted order.
     */
    public Set<String> getStringKeySet() {
        return bibtexStrings.keySet();
    }

    /**
     * Returns a Collection of all BibtexString objects in the database.
     * These are in no particular order.
     */
    public Collection<BibtexString> getStringValues() {
        return bibtexStrings.values();
    }

    /**
     * Returns the string at the given index.
     */
    public synchronized BibtexString getString(String o) {
        return bibtexStrings.get(o);
    }

    /**
     * Returns the number of strings.
     */
    public synchronized int getStringCount() {
        return bibtexStrings.size();
    }

    /**
     * Returns true if a string with the given label already exists.
     */
    public synchronized boolean hasStringLabel(String label) {
        for (BibtexString value : bibtexStrings.values()) {
            if (value.getName().equals(label)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Resolves any references to strings contained in this field content,
     * if possible.
     */
    public String resolveForStrings(String content) {
        if (content == null) {
            throw new IllegalArgumentException("Content for resolveForStrings must not be null.");
        }
        return resolveContent(content, new HashSet<String>());
    }

    /**
     * Take the given collection of BibtexEntry and resolve any string
     * references.
     *
     * @param entries A collection of BibtexEntries in which all strings of the form
     *                #xxx# will be resolved against the hash map of string
     *                references stored in the databasee.
     * @param inPlace If inPlace is true then the given BibtexEntries will be modified, if false then copies of the BibtexEntries are made before resolving the strings.
     * @return a list of bibtexentries, with all strings resolved. It is dependent on the value of inPlace whether copies are made or the given BibtexEntries are modified.
     */
    public List<BibtexEntry> resolveForStrings(Collection<BibtexEntry> entries, boolean inPlace) {

        if (entries == null) {
            throw new NullPointerException();
        }

        List<BibtexEntry> results = new ArrayList<BibtexEntry>(entries.size());

        for (BibtexEntry entry : entries) {
            results.add(this.resolveForStrings(entry, inPlace));
        }
        return results;
    }

    /**
     * Take the given BibtexEntry and resolve any string references.
     *
     * @param entry   A BibtexEntry in which all strings of the form #xxx# will be
     *                resolved against the hash map of string references stored in
     *                the databasee.
     * @param inPlace If inPlace is true then the given BibtexEntry will be
     *                modified, if false then a copy is made using close made before
     *                resolving the strings.
     * @return a BibtexEntry with all string references resolved. It is
     * dependent on the value of inPlace whether a copy is made or the
     * given BibtexEntries is modified.
     */
    public BibtexEntry resolveForStrings(BibtexEntry entry, boolean inPlace) {

        if (!inPlace) {
            entry = (BibtexEntry) entry.clone();
        }

        for (Object field : entry.getAllFields()) {
            entry.setField(field.toString(), this.resolveForStrings(entry.getField(field.toString())));
        }

        return entry;
    }

    /**
     * If the label represents a string contained in this database, returns
     * that string's content. Resolves references to other strings, taking
     * care not to follow a circular reference pattern.
     * If the string is undefined, returns null.
     */
    private String resolveString(String label, HashSet<String> usedIds) {
        for (BibtexString string : bibtexStrings.values()) {

            //Util.pr(label+" : "+string.getName());
            if (string.getName().toLowerCase().equals(label.toLowerCase())) {

                // First check if this string label has been resolved
                // earlier in this recursion. If so, we have a
                // circular reference, and have to stop to avoid
                // infinite recursion.
                if (usedIds.contains(string.getId())) {
                    LOGGER.info("Stopped due to circular reference in strings: " + label);
                    return label;
                }
                // If not, log this string's ID now.
                usedIds.add(string.getId());

                // Ok, we found the string. Now we must make sure we
                // resolve any references to other strings in this one.
                String res = string.getContent();
                res = resolveContent(res, usedIds);

                // Finished with recursing this branch, so we remove our
                // ID again:
                usedIds.remove(string.getId());

                return res;
            }
        }

        // If we get to this point, the string has obviously not been defined locally.
        // Check if one of the standard BibTeX month strings has been used:
        MonthUtil.Month month = MonthUtil.getMonthByShortName(label);
        if (month.isValid()) {
            return month.fullName;
        } else {
            return null;
        }
    }

    private String resolveContent(String res, HashSet<String> usedIds) {
        //if (res.matches(".*#[-\\^\\:\\w]+#.*")) {
        if (res.matches(".*#[^#]+#.*")) {
            StringBuilder newRes = new StringBuilder();
            int piv = 0;
            int next;
            while ((next = res.indexOf("#", piv)) >= 0) {

                // We found the next string ref. Append the text
                // up to it.
                if (next > 0) {
                    newRes.append(res.substring(piv, next));
                }
                int stringEnd = res.indexOf("#", next + 1);
                if (stringEnd >= 0) {
                    // We found the boundaries of the string ref,
                    // now resolve that one.
                    String refLabel = res.substring(next + 1, stringEnd);
                    String resolved = resolveString(refLabel, usedIds);

                    if (resolved == null) {
                        // Could not resolve string. Display the #
                        // characters rather than removing them:
                        newRes.append(res.substring(next, stringEnd + 1));
                    } else {
                        // The string was resolved, so we display its meaning only,
                        // stripping the # characters signifying the string label:
                        newRes.append(resolved);
                    }
                    piv = stringEnd + 1;
                } else {
                    // We didn't find the boundaries of the string ref. This
                    // makes it impossible to interpret it as a string label.
                    // So we should just append the rest of the text and finish.
                    newRes.append(res.substring(next));
                    piv = res.length();
                    break;
                }

            }
            if (piv < res.length() - 1) {
                newRes.append(res.substring(piv));
            }
            res = newRes.toString();
        }
        return res;
    }

    //##########################################
    //  usage:
    //  isDuplicate=checkForDuplicateKeyAndAdd( null, b.getKey() , issueDuplicateWarning);
    //############################################
    // if the newkey already exists and is not the same as oldkey it will give a warning
    // else it will add the newkey to the to set and remove the oldkey
    private boolean checkForDuplicateKeyAndAdd(String oldKey, String newKey) {
        // Globals.logger(" checkForDuplicateKeyAndAdd [oldKey = " + oldKey + "] [newKey = " + newKey + "]");

        boolean duplicate;
        if (oldKey == null) {// this is a new entry so don't bother removing oldKey
            duplicate = addKeyToSet(newKey);
        } else {
            if (oldKey.equals(newKey)) {// were OK because the user did not change keys
                duplicate = false;
            } else {// user changed the key

                // removed the oldkey
                // But what if more than two have the same key?
                // this means that user can add another key and would not get a warning!
                // consider this: i add a key xxx, then i add another key xxx . I get a warning. I delete the key xxx. JBM
                // removes this key from the allKey. then I add another key xxx. I don't get a warning!
                // i need a way to count the number of keys of each type
                // hashmap=>int (increment each time)

                removeKeyFromSet(oldKey);
                duplicate = addKeyToSet(newKey);
            }
        }
        if (duplicate) {
            LOGGER.warn(Localization.lang("Warning there is a duplicate key") + ':' + newKey);
        }
        return duplicate;
    }

    /**
     * Returns the number of occurences of the given key in this database.
     */
    public int getNumberOfKeyOccurences(String key) {
        Object o = allKeys.get(key);
        if (o == null) {
            return 0;
        } else {
            return (Integer) o;
        }

    }

    //========================================================
    // keep track of all the keys to warn if there are duplicates
    //========================================================
    private boolean addKeyToSet(String key) {
        boolean exists = false;
        if (key == null || key.isEmpty()) {
            return false;//don't put empty key
        }
        if (allKeys.containsKey(key)) {
            // warning
            exists = true;
            allKeys.put(key, allKeys.get(key) + 1);// incrementInteger( allKeys.get(key)));
        } else {
            allKeys.put(key, 1);
        }
        return exists;
    }

    //========================================================
    // reduce the number of keys by 1. if this number goes to zero then remove from the set
    // note: there is a good reason why we should not use a hashset but use hashmap instead
    //========================================================
    private void removeKeyFromSet(String key) {
        if (key == null || key.isEmpty()) {
            return;
        }
        if (allKeys.containsKey(key)) {
            Integer tI = allKeys.get(key); // if(allKeys.get(key) instanceof Integer)
            if (tI == 1) {
                allKeys.remove(key);
            } else {
                allKeys.put(key, tI - 1);//decrementInteger( tI ));
            }
        }
    }

    private void fireDatabaseChanged(DatabaseChangeEvent e) {
        for (DatabaseChangeListener listener : changeListeners) {
            listener.databaseChanged(e);
        }
    }

    public void addDatabaseChangeListener(DatabaseChangeListener l) {
        changeListeners.add(l);
    }

    public void removeDatabaseChangeListener(DatabaseChangeListener l) {
        changeListeners.remove(l);
    }

    /**
     * Returns the text stored in the given field of the given bibtex entry
     * which belongs to the given database.
     * <p>
     * If a database is given, this function will try to resolve any string
     * references in the field-value.
     * Also, if a database is given, this function will try to find values for
     * unset fields in the entry linked by the "crossref" field, if any.
     *
     * @param field    The field to return the value of.
     * @param bibtex   maybenull
     *                 The bibtex entry which contains the field.
     * @param database maybenull
     *                 The database of the bibtex entry.
     * @return The resolved field value or null if not found.
     */
    public static String getResolvedField(String field, BibtexEntry bibtex,
                                          BibtexDatabase database) {

        if (field.equals("bibtextype")) {
            return bibtex.getType().getName();
        }

        // TODO: Changed this to also consider alias fields, which is the expected 
        // behavior for the preview layout and for the check whatever all fields are present.
        // But there might be unwanted side-effects?!
        Object o = bibtex.getFieldOrAlias(field);

        // If this field is not set, and the entry has a crossref, try to look up the
        // field in the referred entry: Do not do this for the bibtex key.
        if (o == null && database != null && database.followCrossrefs && !field.equals(BibtexFields.KEY_FIELD)) {
            Object crossRef = bibtex.getField("crossref");
            if (crossRef != null) {
                BibtexEntry referred = database.getEntryByKey((String) crossRef);
                if (referred != null) {
                    // Ok, we found the referred entry. Get the field value from that
                    // entry. If it is unset there, too, stop looking:
                    o = referred.getField(field);
                }
            }
        }

        return BibtexDatabase.getText((String) o, database);
    }

    /**
     * Returns a text with references resolved according to an optionally given
     * database.
     *
     * @param toResolve maybenull The text to resolve.
     * @param database  maybenull The database to use for resolving the text.
     * @return The resolved text or the original text if either the text or the database are null
     */
    public static String getText(String toResolve, BibtexDatabase database) {
        if (toResolve != null && database != null) {
            return database.resolveForStrings(toResolve);
        }

        return toResolve;
    }

    public void setFollowCrossrefs(boolean followCrossrefs) {
        this.followCrossrefs = followCrossrefs;
    }
}
<commit_msg>Replace Hashtable with ConcurrentHashMap<commit_after>/*
Copyright (C) 2003 <NAME>, <NAME>

All programs in this directory and
subdirectories are published under the GNU General Public License as
described below.

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU 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
General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA

Further information about the GNU GPL is available at:
http://www.gnu.org/copyleft/gpl.ja.html

Note:
Modified for use in JabRef

*/
package net.sf.jabref.model;

import net.sf.jabref.*;
import net.sf.jabref.logic.l10n.Localization;
import net.sf.jabref.logic.util.MonthUtil;

import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class BibtexDatabase {

    private final Map<String, BibtexEntry> entries = new ConcurrentHashMap<>();

    private String preamble;

    private final HashMap<String, BibtexString> bibtexStrings = new HashMap<>();

    private final Set<DatabaseChangeListener> changeListeners = new HashSet<>();

    private boolean followCrossrefs = true;

    private static final Log LOGGER = LogFactory.getLog(BibtexDatabase.class);

    /**
     * use a map instead of a set since i need to know how many of each key is
     * inthere
     */
    private final HashMap<String, Integer> allKeys = new HashMap<>();

    /*
     * Entries are stored in a HashMap with the ID as key. What happens if
     * someone changes a BibtexEntry's ID after it has been added to this
     * BibtexDatabase? The key of that entry would be the old ID, not the new
     * one. Use a PropertyChangeListener to identify an ID change and update the
     * Map.
     */
    private final VetoableChangeListener listener =
            propertyChangeEvent -> {
                if (propertyChangeEvent.getPropertyName() == null) {
                    fireDatabaseChanged(new DatabaseChangeEvent(BibtexDatabase.this, DatabaseChangeEvent.ChangeType.CHANGING_ENTRY, (BibtexEntry) propertyChangeEvent.getSource()));
                } else if ("id".equals(propertyChangeEvent.getPropertyName())) {
                    // locate the entry under its old key
                    BibtexEntry oldEntry =
                            entries.remove(propertyChangeEvent.getOldValue());

                    if (oldEntry != propertyChangeEvent.getSource()) {
                        // Something is very wrong!
                        // The entry under the old key isn't
                        // the one that sent this event.
                        // Restore the old state.
                        entries.put((String) propertyChangeEvent.getOldValue(), oldEntry);
                        throw new PropertyVetoException("Wrong old ID", propertyChangeEvent);
                    }

                    if (entries.get(propertyChangeEvent.getNewValue()) != null) {
                        entries.put((String) propertyChangeEvent.getOldValue(), oldEntry);
                        throw new PropertyVetoException
                                ("New ID already in use, please choose another",
                                        propertyChangeEvent);
                    }

                    // and re-file this entry
                    entries.put((String) propertyChangeEvent.getNewValue(),
                            (BibtexEntry) propertyChangeEvent.getSource());
                } else {
                    fireDatabaseChanged(new DatabaseChangeEvent(BibtexDatabase.this, DatabaseChangeEvent.ChangeType.CHANGED_ENTRY, (BibtexEntry) propertyChangeEvent.getSource()));
                }
            };


    /**
     * Returns the number of entries.
     */
    public synchronized int getEntryCount() {
        return entries.size();
    }

    /**
     * Returns a Set containing the keys to all entries.
     * Use getKeySet().iterator() to iterate over all entries.
     */
    public synchronized Set<String> getKeySet() {
        return entries.keySet();
    }

    /**
     * Returns an EntrySorter with the sorted entries from this base,
     * sorted by the given Comparator.
     */
    public synchronized EntrySorter getSorter(Comparator<BibtexEntry> comp) {
        EntrySorter sorter = new EntrySorter(entries, comp);
        addDatabaseChangeListener(sorter);
        return sorter;
    }

    /**
     * Just temporary, for testing purposes....
     *
     * @return
     */
    public Map<String, BibtexEntry> getEntryMap() {
        return entries;
    }

    /**
     * Returns the entry with the given ID (-> entry_type + hashcode).
     */
    public synchronized BibtexEntry getEntryById(String id) {
        return entries.get(id);
    }

    public synchronized Collection<BibtexEntry> getEntries() {
        return entries.values();
    }

    /**
     * Returns the entry with the given bibtex key.
     */
    public synchronized BibtexEntry getEntryByKey(String key) {
        BibtexEntry back = null;

        int keyHash = key.hashCode(); // key hash for better performance

        Set<String> keySet = entries.keySet();
        for (String entrieID : keySet) {
            BibtexEntry entry = getEntryById(entrieID);
            if (entry != null && entry.getCiteKey() != null) {
                String citeKey = entry.getCiteKey();
                if (citeKey != null) {
                    if (keyHash == citeKey.hashCode()) {
                        back = entry;
                    }
                }
            }
        }
        return back;
    }

    public synchronized BibtexEntry[] getEntriesByKey(String key) {

        ArrayList<BibtexEntry> entries = new ArrayList<BibtexEntry>();

        for (BibtexEntry entry : this.entries.values()) {
            if (key.equals(entry.getCiteKey())) {
                entries.add(entry);
            }
        }

        return entries.toArray(new BibtexEntry[entries.size()]);
    }

    /**
     * Inserts the entry, given that its ID is not already in use.
     * use Util.createId(...) to make up a unique ID for an entry.
     */
    public synchronized boolean insertEntry(BibtexEntry entry)
            throws KeyCollisionException {
        String id = entry.getId();
        if (getEntryById(id) != null) {
            throw new KeyCollisionException(
                    "ID is already in use, please choose another");
        }

        entry.addPropertyChangeListener(listener);

        entries.put(id, entry);

        fireDatabaseChanged(new DatabaseChangeEvent(this, DatabaseChangeEvent.ChangeType.ADDED_ENTRY, entry));

        return checkForDuplicateKeyAndAdd(null, entry.getCiteKey());
    }

    /**
     * Removes the entry with the given string.
     * <p>
     * Returns null if not found.
     */
    public synchronized BibtexEntry removeEntry(String id) {
        BibtexEntry oldValue = entries.remove(id);

        if (oldValue == null) {
            return null;
        }

        removeKeyFromSet(oldValue.getCiteKey());
        oldValue.removePropertyChangeListener(listener);
        fireDatabaseChanged(new DatabaseChangeEvent(this, DatabaseChangeEvent.ChangeType.REMOVED_ENTRY, oldValue));

        return oldValue;
    }

    public synchronized boolean setCiteKeyForEntry(String id, String key) {
        if (!entries.containsKey(id)) {
            return false; // Entry doesn't exist!
        }
        BibtexEntry entry = getEntryById(id);
        String oldKey = entry.getCiteKey();
        if (key != null) {
            entry.setField(BibtexFields.KEY_FIELD, key);
        } else {
            entry.clearField(BibtexFields.KEY_FIELD);
        }
        return checkForDuplicateKeyAndAdd(oldKey, entry.getCiteKey());
    }

    /**
     * Sets the database's preamble.
     */
    public synchronized void setPreamble(String preamble) {
        this.preamble = preamble;
    }

    /**
     * Returns the database's preamble.
     */
    public synchronized String getPreamble() {
        return preamble;
    }

    /**
     * Inserts a Bibtex String at the given index.
     */
    public synchronized void addString(BibtexString string)
            throws KeyCollisionException {
        if (hasStringLabel(string.getName())) {
            throw new KeyCollisionException(Localization.lang("A string with this label already exists"));
        }

        if (bibtexStrings.containsKey(string.getId())) {
            throw new KeyCollisionException("Duplicate BibtexString id.");
        }

        bibtexStrings.put(string.getId(), string);
    }

    /**
     * Removes the string at the given index.
     */
    public synchronized void removeString(String id) {
        bibtexStrings.remove(id);
    }

    /**
     * Returns a Set of keys to all BibtexString objects in the database.
     * These are in no sorted order.
     */
    public Set<String> getStringKeySet() {
        return bibtexStrings.keySet();
    }

    /**
     * Returns a Collection of all BibtexString objects in the database.
     * These are in no particular order.
     */
    public Collection<BibtexString> getStringValues() {
        return bibtexStrings.values();
    }

    /**
     * Returns the string at the given index.
     */
    public synchronized BibtexString getString(String o) {
        return bibtexStrings.get(o);
    }

    /**
     * Returns the number of strings.
     */
    public synchronized int getStringCount() {
        return bibtexStrings.size();
    }

    /**
     * Returns true if a string with the given label already exists.
     */
    public synchronized boolean hasStringLabel(String label) {
        for (BibtexString value : bibtexStrings.values()) {
            if (value.getName().equals(label)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Resolves any references to strings contained in this field content,
     * if possible.
     */
    public String resolveForStrings(String content) {
        if (content == null) {
            throw new IllegalArgumentException("Content for resolveForStrings must not be null.");
        }
        return resolveContent(content, new HashSet<String>());
    }

    /**
     * Take the given collection of BibtexEntry and resolve any string
     * references.
     *
     * @param entries A collection of BibtexEntries in which all strings of the form
     *                #xxx# will be resolved against the hash map of string
     *                references stored in the databasee.
     * @param inPlace If inPlace is true then the given BibtexEntries will be modified, if false then copies of the BibtexEntries are made before resolving the strings.
     * @return a list of bibtexentries, with all strings resolved. It is dependent on the value of inPlace whether copies are made or the given BibtexEntries are modified.
     */
    public List<BibtexEntry> resolveForStrings(Collection<BibtexEntry> entries, boolean inPlace) {

        if (entries == null) {
            throw new NullPointerException();
        }

        List<BibtexEntry> results = new ArrayList<BibtexEntry>(entries.size());

        for (BibtexEntry entry : entries) {
            results.add(this.resolveForStrings(entry, inPlace));
        }
        return results;
    }

    /**
     * Take the given BibtexEntry and resolve any string references.
     *
     * @param entry   A BibtexEntry in which all strings of the form #xxx# will be
     *                resolved against the hash map of string references stored in
     *                the databasee.
     * @param inPlace If inPlace is true then the given BibtexEntry will be
     *                modified, if false then a copy is made using close made before
     *                resolving the strings.
     * @return a BibtexEntry with all string references resolved. It is
     * dependent on the value of inPlace whether a copy is made or the
     * given BibtexEntries is modified.
     */
    public BibtexEntry resolveForStrings(BibtexEntry entry, boolean inPlace) {

        if (!inPlace) {
            entry = (BibtexEntry) entry.clone();
        }

        for (Object field : entry.getAllFields()) {
            entry.setField(field.toString(), this.resolveForStrings(entry.getField(field.toString())));
        }

        return entry;
    }

    /**
     * If the label represents a string contained in this database, returns
     * that string's content. Resolves references to other strings, taking
     * care not to follow a circular reference pattern.
     * If the string is undefined, returns null.
     */
    private String resolveString(String label, HashSet<String> usedIds) {
        for (BibtexString string : bibtexStrings.values()) {

            //Util.pr(label+" : "+string.getName());
            if (string.getName().toLowerCase().equals(label.toLowerCase())) {

                // First check if this string label has been resolved
                // earlier in this recursion. If so, we have a
                // circular reference, and have to stop to avoid
                // infinite recursion.
                if (usedIds.contains(string.getId())) {
                    LOGGER.info("Stopped due to circular reference in strings: " + label);
                    return label;
                }
                // If not, log this string's ID now.
                usedIds.add(string.getId());

                // Ok, we found the string. Now we must make sure we
                // resolve any references to other strings in this one.
                String res = string.getContent();
                res = resolveContent(res, usedIds);

                // Finished with recursing this branch, so we remove our
                // ID again:
                usedIds.remove(string.getId());

                return res;
            }
        }

        // If we get to this point, the string has obviously not been defined locally.
        // Check if one of the standard BibTeX month strings has been used:
        MonthUtil.Month month = MonthUtil.getMonthByShortName(label);
        if (month.isValid()) {
            return month.fullName;
        } else {
            return null;
        }
    }

    private String resolveContent(String res, HashSet<String> usedIds) {
        //if (res.matches(".*#[-\\^\\:\\w]+#.*")) {
        if (res.matches(".*#[^#]+#.*")) {
            StringBuilder newRes = new StringBuilder();
            int piv = 0;
            int next;
            while ((next = res.indexOf("#", piv)) >= 0) {

                // We found the next string ref. Append the text
                // up to it.
                if (next > 0) {
                    newRes.append(res.substring(piv, next));
                }
                int stringEnd = res.indexOf("#", next + 1);
                if (stringEnd >= 0) {
                    // We found the boundaries of the string ref,
                    // now resolve that one.
                    String refLabel = res.substring(next + 1, stringEnd);
                    String resolved = resolveString(refLabel, usedIds);

                    if (resolved == null) {
                        // Could not resolve string. Display the #
                        // characters rather than removing them:
                        newRes.append(res.substring(next, stringEnd + 1));
                    } else {
                        // The string was resolved, so we display its meaning only,
                        // stripping the # characters signifying the string label:
                        newRes.append(resolved);
                    }
                    piv = stringEnd + 1;
                } else {
                    // We didn't find the boundaries of the string ref. This
                    // makes it impossible to interpret it as a string label.
                    // So we should just append the rest of the text and finish.
                    newRes.append(res.substring(next));
                    piv = res.length();
                    break;
                }

            }
            if (piv < res.length() - 1) {
                newRes.append(res.substring(piv));
            }
            res = newRes.toString();
        }
        return res;
    }

    //##########################################
    //  usage:
    //  isDuplicate=checkForDuplicateKeyAndAdd( null, b.getKey() , issueDuplicateWarning);
    //############################################
    // if the newkey already exists and is not the same as oldkey it will give a warning
    // else it will add the newkey to the to set and remove the oldkey
    private boolean checkForDuplicateKeyAndAdd(String oldKey, String newKey) {
        // Globals.logger(" checkForDuplicateKeyAndAdd [oldKey = " + oldKey + "] [newKey = " + newKey + "]");

        boolean duplicate;
        if (oldKey == null) {// this is a new entry so don't bother removing oldKey
            duplicate = addKeyToSet(newKey);
        } else {
            if (oldKey.equals(newKey)) {// were OK because the user did not change keys
                duplicate = false;
            } else {// user changed the key

                // removed the oldkey
                // But what if more than two have the same key?
                // this means that user can add another key and would not get a warning!
                // consider this: i add a key xxx, then i add another key xxx . I get a warning. I delete the key xxx. JBM
                // removes this key from the allKey. then I add another key xxx. I don't get a warning!
                // i need a way to count the number of keys of each type
                // hashmap=>int (increment each time)

                removeKeyFromSet(oldKey);
                duplicate = addKeyToSet(newKey);
            }
        }
        if (duplicate) {
            LOGGER.warn(Localization.lang("Warning there is a duplicate key") + ':' + newKey);
        }
        return duplicate;
    }

    /**
     * Returns the number of occurences of the given key in this database.
     */
    public int getNumberOfKeyOccurences(String key) {
        Object o = allKeys.get(key);
        if (o == null) {
            return 0;
        } else {
            return (Integer) o;
        }

    }

    //========================================================
    // keep track of all the keys to warn if there are duplicates
    //========================================================
    private boolean addKeyToSet(String key) {
        boolean exists = false;
        if (key == null || key.isEmpty()) {
            return false;//don't put empty key
        }
        if (allKeys.containsKey(key)) {
            // warning
            exists = true;
            allKeys.put(key, allKeys.get(key) + 1);// incrementInteger( allKeys.get(key)));
        } else {
            allKeys.put(key, 1);
        }
        return exists;
    }

    //========================================================
    // reduce the number of keys by 1. if this number goes to zero then remove from the set
    // note: there is a good reason why we should not use a hashset but use hashmap instead
    //========================================================
    private void removeKeyFromSet(String key) {
        if (key == null || key.isEmpty()) {
            return;
        }
        if (allKeys.containsKey(key)) {
            Integer tI = allKeys.get(key); // if(allKeys.get(key) instanceof Integer)
            if (tI == 1) {
                allKeys.remove(key);
            } else {
                allKeys.put(key, tI - 1);//decrementInteger( tI ));
            }
        }
    }

    private void fireDatabaseChanged(DatabaseChangeEvent e) {
        for (DatabaseChangeListener listener : changeListeners) {
            listener.databaseChanged(e);
        }
    }

    public void addDatabaseChangeListener(DatabaseChangeListener l) {
        changeListeners.add(l);
    }

    public void removeDatabaseChangeListener(DatabaseChangeListener l) {
        changeListeners.remove(l);
    }

    /**
     * Returns the text stored in the given field of the given bibtex entry
     * which belongs to the given database.
     * <p>
     * If a database is given, this function will try to resolve any string
     * references in the field-value.
     * Also, if a database is given, this function will try to find values for
     * unset fields in the entry linked by the "crossref" field, if any.
     *
     * @param field    The field to return the value of.
     * @param bibtex   maybenull
     *                 The bibtex entry which contains the field.
     * @param database maybenull
     *                 The database of the bibtex entry.
     * @return The resolved field value or null if not found.
     */
    public static String getResolvedField(String field, BibtexEntry bibtex,
                                          BibtexDatabase database) {

        if (field.equals("bibtextype")) {
            return bibtex.getType().getName();
        }

        // TODO: Changed this to also consider alias fields, which is the expected 
        // behavior for the preview layout and for the check whatever all fields are present.
        // But there might be unwanted side-effects?!
        Object o = bibtex.getFieldOrAlias(field);

        // If this field is not set, and the entry has a crossref, try to look up the
        // field in the referred entry: Do not do this for the bibtex key.
        if (o == null && database != null && database.followCrossrefs && !field.equals(BibtexFields.KEY_FIELD)) {
            Object crossRef = bibtex.getField("crossref");
            if (crossRef != null) {
                BibtexEntry referred = database.getEntryByKey((String) crossRef);
                if (referred != null) {
                    // Ok, we found the referred entry. Get the field value from that
                    // entry. If it is unset there, too, stop looking:
                    o = referred.getField(field);
                }
            }
        }

        return BibtexDatabase.getText((String) o, database);
    }

    /**
     * Returns a text with references resolved according to an optionally given
     * database.
     *
     * @param toResolve maybenull The text to resolve.
     * @param database  maybenull The database to use for resolving the text.
     * @return The resolved text or the original text if either the text or the database are null
     */
    public static String getText(String toResolve, BibtexDatabase database) {
        if (toResolve != null && database != null) {
            return database.resolveForStrings(toResolve);
        }

        return toResolve;
    }

    public void setFollowCrossrefs(boolean followCrossrefs) {
        this.followCrossrefs = followCrossrefs;
    }
}

{'fraction_non_alphanumeric': 0.09768702872151148, 'fraction_numerical': 0.0015250360077946285, 'mean_word_length': 1.965826999183366, 'pattern_counts': {'":': 0, '<': 61, '<?xml version=': 0, '>': 71, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4149090', 'n_tokens_mistral': 12222, 'n_tokens_neox': 11566, 'n_words': 4117}