6208906c2aa9787c84af056423aa83e6cbf7f676
[debian/jabref.git] / src / java / net / sf / jabref / BibtexDatabase.java
1 /*
2 Copyright (C) 2003 David Weitzman, Morten O. Alver
3
4 All programs in this directory and
5 subdirectories are published under the GNU General Public License as
6 described below.
7
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License, or (at
11 your option) any later version.
12
13 This program is distributed in the hope that it will be useful, but
14 WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
21 USA
22
23 Further information about the GNU GPL is available at:
24 http://www.gnu.org/copyleft/gpl.ja.html
25
26 Note:
27 Modified for use in JabRef
28
29 */
30
31
32 // created by : ?
33 //
34 // modified : r.nagel 23.08.2004
35 //                - insert getEntryByKey() methode needed by AuxSubGenerator
36
37 package net.sf.jabref;
38
39 import java.beans.*;
40 import java.util.*;
41
42 import javax.swing.JOptionPane;
43
44 import net.sf.jabref.groups.GroupSelector;
45
46 public class BibtexDatabase
47 {
48     Map _entries = new Hashtable();
49     String _preamble = null;
50     HashMap _strings = new HashMap();
51     Vector _strings_ = new Vector();
52     Hashtable _autoCompleters = null;
53     Set changeListeners = new HashSet();
54     private BibtexDatabase ths = this;
55
56     private HashMap allKeys  = new HashMap();   // use a map instead of a set since i need to know how many of each key is inthere
57
58     /* Entries are stored in a HashMap with the ID as key.
59      * What happens if someone changes a BibtexEntry's ID
60      * after it has been added to this BibtexDatabase?
61      * The key of that entry would be the old ID, not the new one.
62      * Use a PropertyChangeListener to identify an ID change
63      * and update the Map.
64      */
65     private final VetoableChangeListener listener =
66         new VetoableChangeListener()
67         {
68             public void vetoableChange(PropertyChangeEvent pce)
69                 throws PropertyVetoException
70             {
71                 if (pce.getPropertyName() == null)
72                     fireDatabaseChanged (new DatabaseChangeEvent(ths, DatabaseChangeEvent.CHANGING_ENTRY, (BibtexEntry)pce.getSource()));
73                 else if ("id".equals(pce.getPropertyName()))
74                 {
75                     // locate the entry under its old key
76                     Object oldEntry =
77                         _entries.remove((String) pce.getOldValue());
78
79                     if (oldEntry != pce.getSource())
80                     {
81                         // Something is very wrong!
82                         // The entry under the old key isn't
83                         // the one that sent this event.
84                         // Restore the old state.
85                         _entries.put(pce.getOldValue(), oldEntry);
86                         throw new PropertyVetoException("Wrong old ID", pce);
87                     }
88
89                     if (_entries.get(pce.getNewValue()) != null)
90                     {
91                         _entries.put(pce.getOldValue(), oldEntry);
92                         throw new PropertyVetoException
93                             ("New ID already in use, please choose another",
94                             pce);
95                     }
96
97                     // and re-file this entry
98                     _entries.put((String) pce.getNewValue(),
99                         (BibtexEntry) pce.getSource());
100                 } else {
101                     fireDatabaseChanged (new DatabaseChangeEvent(ths, DatabaseChangeEvent.CHANGED_ENTRY, (BibtexEntry)pce.getSource()));
102                     //Util.pr(pce.getSource().toString()+"\n"+pce.getPropertyName()
103                     //    +"\n"+pce.getNewValue());
104                 }
105             }
106         };
107
108     /**
109      * Returns the number of entries.
110      */
111     public synchronized int getEntryCount()
112     {
113         return _entries.size();
114     }
115
116     /**
117      * Returns a Set containing the keys to all entries.
118      * Use getKeySet().iterator() to iterate over all entries.
119      */
120     public synchronized Set getKeySet()
121     {
122         return _entries.keySet();
123     }
124
125     /**
126      * Returns an EntrySorter with the sorted entries from this base,
127      * sorted by the given Comparator.
128      */
129     public synchronized EntrySorter getSorter(java.util.Comparator comp) {
130         EntrySorter sorter = new EntrySorter(_entries, comp);
131         addDatabaseChangeListener(sorter);
132         return sorter;
133     }
134
135     /**
136      * Just temporary, for testing purposes....
137      * @return
138      */
139     public Map getEntryMap() { return _entries; }
140
141     /**
142      * Returns the entry with the given ID (-> entry_type + hashcode).
143      */
144     public synchronized BibtexEntry getEntryById(String id)
145     {
146         return (BibtexEntry) _entries.get(id);
147     }
148     
149     public synchronized Collection getEntries() {
150         return _entries.values();
151     }
152
153     /**
154      * Returns the entry with the given bibtex key.
155      */
156     public synchronized BibtexEntry getEntryByKey(String key)
157     {
158       BibtexEntry back = null ;
159
160       int keyHash = key.hashCode() ; // key hash for better performance
161
162       Set keySet = _entries.keySet();
163       if (keySet != null)
164       {
165           Iterator it = keySet.iterator();
166           boolean loop = it.hasNext() ;
167           while(loop)
168           {
169             String entrieID = (String) it.next() ;
170             BibtexEntry entry = getEntryById(entrieID) ;
171             if ((entry != null) && (entry.getCiteKey() != null))
172             {
173               String citeKey = entry.getCiteKey() ;
174               if (citeKey != null)
175               {
176                 if (keyHash == citeKey.hashCode() )
177                 {
178                   loop = false ;
179                   back = entry ;
180                 }
181                 else loop = it.hasNext() ;
182               } else loop = it.hasNext() ;
183             }
184           }
185       }
186       return back ;
187     }
188     
189     public synchronized BibtexEntry[] getEntriesByKey(String key) {
190         Vector entries = new Vector();
191         BibtexEntry entry;
192         for (Iterator it = _entries.entrySet().iterator(); it.hasNext(); ) {
193             entry = (BibtexEntry)((Map.Entry)it.next()).getValue();
194             if (key.equals(entry.getCiteKey()))
195                 entries.add(entry);
196         }
197         BibtexEntry[] entryArray = new BibtexEntry[entries.size()];
198         return (BibtexEntry[]) entries.toArray(entryArray);
199     }    
200
201     /**
202      * Inserts the entry, given that its ID is not already in use.
203      * use Util.createId(...) to make up a unique ID for an entry.
204      */
205     public synchronized boolean insertEntry(BibtexEntry entry)
206         throws KeyCollisionException
207     {
208         String id = entry.getId();
209         if (getEntryById(id) != null)
210         {
211           throw new KeyCollisionException(
212                 "ID is already in use, please choose another");
213         }
214
215         entry.addPropertyChangeListener(listener);
216
217         // Possibly add a FieldChangeListener, which is there to add
218         // new words to the autocompleter's dictionary. In case the
219         // entry is non-empty (pasted), update completers.
220         /*if (_autoCompleters != null) {
221             entry.addPropertyChangeListener(new FieldChangeListener
222                                             (_autoCompleters, entry));
223             Util.updateCompletersForEntry(_autoCompleters,
224                                           entry);
225         }
226         */
227         _entries.put(id, entry);
228
229         fireDatabaseChanged(new DatabaseChangeEvent(this, DatabaseChangeEvent.ADDED_ENTRY, entry));
230
231         return checkForDuplicateKeyAndAdd(null, entry.getCiteKey(), false);
232     }
233
234     /**
235      * Removes the entry with the given string.
236      */
237     public synchronized BibtexEntry removeEntry(String id)
238     {
239         BibtexEntry oldValue = (BibtexEntry) _entries.remove(id);
240         removeKeyFromSet(oldValue.getCiteKey());
241
242         if (oldValue != null)
243         {
244             oldValue.removePropertyChangeListener(listener);
245         }
246
247         fireDatabaseChanged(new DatabaseChangeEvent(this, DatabaseChangeEvent.REMOVED_ENTRY, oldValue));
248
249         return oldValue;
250     }
251
252     public synchronized boolean setCiteKeyForEntry(String id, String key) {
253         if (!_entries.containsKey(id)) return false; // Entry doesn't exist!
254         BibtexEntry entry = getEntryById(id);
255         String oldKey = entry.getCiteKey();
256         if (key != null)
257           entry.setField(Globals.KEY_FIELD, key);
258         else
259           entry.clearField(Globals.KEY_FIELD);
260         return checkForDuplicateKeyAndAdd(oldKey, entry.getCiteKey(), false);
261     }
262
263     /**
264      * Sets the database's preamble.
265      */
266     public synchronized void setPreamble(String preamble)
267     {
268         _preamble = preamble;
269     }
270
271     /**
272      * Returns the database's preamble.
273      */
274     public synchronized String getPreamble()
275     {
276         return _preamble;
277     }
278
279     /**
280      * Inserts a Bibtex String at the given index.
281      */
282     public synchronized void addString(BibtexString string)
283         throws KeyCollisionException
284     {
285         for (java.util.Iterator i=_strings.keySet().iterator(); i.hasNext();) {
286             if (((BibtexString)_strings.get(i.next())).getName().equals(string.getName()))
287                 throw new KeyCollisionException("A string with this label already exists,");
288         }
289         
290         if (_strings.containsKey(string.getId()))
291             throw new KeyCollisionException("Duplicate BibtexString id.");
292         
293         _strings.put(string.getId(), string);
294     }
295
296     /**
297      * Removes the string at the given index.
298      */
299     public synchronized void removeString(String id) {
300         _strings.remove(id);
301     }
302
303     /**
304      * Returns a Set of keys to all BibtexString objects in the database.
305      * These are in no sorted order.
306      */
307     public Set getStringKeySet() {
308         return _strings.keySet();
309     }
310
311     /**
312      * Returns the string at the given index.
313      */
314     public synchronized BibtexString getString(Object o) {
315         return (BibtexString)(_strings.get(o));
316     }
317
318     /**
319      * Returns the number of strings.
320      */
321     public synchronized int getStringCount() {
322         return _strings.size();
323     }
324
325     /**
326      * Returns true if a string with the given label already exists.
327      */
328     public synchronized boolean hasStringLabel(String label) {
329         for (java.util.Iterator i=_strings.keySet().iterator(); i.hasNext();) {
330             if (((BibtexString)_strings.get(i.next())).getName().equals(label))
331                 return true;
332         }
333         return false;
334     }
335
336    /**
337     * If the label represents a string contained in this database, returns
338     * that string's content. Resolves references to other strings, taking
339     * care not to follow a circular reference pattern.
340     * If the string is undefined, returns the label itself.
341     */
342     public String resolveString(String label) {
343         return resolveString(label, new HashSet());
344     }
345    
346     /**
347      * Resolves any references to strings contained in this database,
348      * if possible.
349      */
350     public String resolveForStrings(String content) {
351         return resolveContent(content, new HashSet());
352     }
353
354     private String resolveString(String label, HashSet usedIds) {
355
356         for (java.util.Iterator i=_strings.keySet().iterator(); i.hasNext();) {
357             BibtexString string = (BibtexString)_strings.get(i.next());
358
359                 //Util.pr(label+" : "+string.getName());
360             if (string.getName().toLowerCase().equals(label.toLowerCase())) {
361
362                 // First check if this string label has been resolved
363                 // earlier in this recursion. If so, we have a
364                 // circular reference, and have to stop to avoid
365                 // infinite recursion.
366                 if (usedIds.contains(string.getId())) {
367                     Util.pr("Stopped due to circular reference in strings: "+label);
368                     return label;
369                 }
370                 // If not, log this string's ID now.
371                 usedIds.add(string.getId());
372
373                 // Ok, we found the string. Now we must make sure we
374                 // resolve any references to other strings in this one.
375                 String res = string.getContent();
376                 res = resolveContent(res, usedIds);
377
378                 // Finished with recursing this branch, so we remove our
379                 // ID again:
380                 usedIds.remove(string.getId());
381
382                 return res;
383             }
384         }
385         
386         // If we get to this point, the string has obviously not been defined locally.
387         // Check if one of the standard BibTeX month strings has been used:
388         Object o;
389         if ((o = Globals.MONTH_STRINGS.get(label.toLowerCase())) != null) {
390             return (String)o;
391         }
392         
393         return label;
394     }
395
396     private String resolveContent(String res, HashSet usedIds) {
397         //if (res.matches(".*#[-\\^\\:\\w]+#.*")) {
398     if (res.matches(".*#[^#]+#.*")) {
399             StringBuffer newRes = new StringBuffer();
400             int piv = 0, next = 0;
401             while ((next=res.indexOf("#", piv)) >= 0) {
402                 // We found the next string ref. Append the text
403                 // up to it.
404                 if (next > 0)
405                     newRes.append(res.substring(piv, next));
406                 int stringEnd = res.indexOf("#", next+1);
407                 if (stringEnd >= 0) {
408                     // We found the boundaries of the string ref,
409                     // now resolve that one.
410                     String refLabel = res.substring(next+1, stringEnd);
411                     newRes.append(resolveString(refLabel, usedIds));
412                 }
413                 piv = stringEnd+1;
414             }
415             if (piv < res.length()-1)
416                 newRes.append(res.substring(piv));
417             res = newRes.toString();
418         }
419         return res;
420     }
421
422     //##########################################
423     //  usage:
424     //  isDuplicate=checkForDuplicateKeyAndAdd( null, b.getKey() , issueDuplicateWarning);
425     //############################################
426         // if the newkey already exists and is not the same as oldkey it will give a warning
427     // else it will add the newkey to the to set and remove the oldkey
428     public boolean checkForDuplicateKeyAndAdd(String oldKey, String newKey, boolean issueWarning){
429                 // Globals.logger(" checkForDuplicateKeyAndAdd [oldKey = " + oldKey + "] [newKey = " + newKey + "]");
430
431         boolean duplicate=false;
432         if(oldKey==null){// this is a new entry so don't bother removing oldKey
433             duplicate= addKeyToSet( newKey);
434         }else{
435             if(oldKey.equals(newKey)){// were OK because the user did not change keys
436                 duplicate=false;
437             }else{// user changed the key
438
439                 // removed the oldkey
440                 // But what if more than two have the same key?
441                 // this means that user can add another key and would not get a warning!
442                 // consider this: i add a key xxx, then i add another key xxx . I get a warning. I delete the key xxx. JBM
443                 // removes this key from the allKey. then I add another key xxx. I don't get a warning!
444                 // i need a way to count the number of keys of each type
445                 // hashmap=>int (increment each time)
446
447                 removeKeyFromSet( oldKey);
448                 duplicate = addKeyToSet( newKey );
449             }
450         }
451         if(duplicate==true && issueWarning==true){
452             JOptionPane.showMessageDialog(null,  Globals.lang("Warning there is a duplicate key")+":" + newKey ,
453                                           Globals.lang("Duplicate Key Warning"),
454                                           JOptionPane.WARNING_MESSAGE);//, options);
455
456         }
457         return duplicate;
458     }
459
460     /**
461      * Returns the number of occurences of the given key in this database.
462      */
463     public int getNumberOfKeyOccurences(String key) {
464         Object o = allKeys.get(key);
465         if (o == null)
466             return 0;
467         else
468             return ((Integer)o).intValue();
469             
470     }
471     
472     //========================================================
473     // keep track of all the keys to warn if there are duplicates
474     //========================================================
475     private boolean addKeyToSet(String key){
476                 boolean exists=false;
477                 if((key == null) || key.equals(""))
478                         return false;//don't put empty key
479                 if(allKeys.containsKey(key)){
480                         // warning
481                         exists=true;
482                         allKeys.put( key, new Integer( ((Integer)allKeys.get(key)).intValue() + 1));// incrementInteger( allKeys.get(key)));
483                 }else
484                         allKeys.put( key, new Integer(1));
485                 return exists;
486     }
487     //========================================================
488     // reduce the number of keys by 1. if this number goes to zero then remove from the set
489     // note: there is a good reason why we should not use a hashset but use hashmap instead
490     //========================================================
491     private void removeKeyFromSet(String key){
492                 if((key == null) || key.equals("")) return;
493                 if(allKeys.containsKey(key)){
494                         Integer tI = (Integer)allKeys.get(key); // if(allKeys.get(key) instanceof Integer)
495                         if(tI.intValue()==1)
496                                 allKeys.remove( key);
497                         else
498                                 allKeys.put( key, new Integer( ((Integer)tI).intValue() - 1));//decrementInteger( tI ));
499                 }
500     }
501
502
503     
504     public void fireDatabaseChanged(DatabaseChangeEvent e) {
505         for (Iterator i=changeListeners.iterator(); i.hasNext();) {
506             ((DatabaseChangeListener)i.next()).databaseChanged(e);
507         }
508     }
509
510     public void addDatabaseChangeListener(DatabaseChangeListener l) {
511         changeListeners.add(l);
512     }
513
514     public void removeDatabaseChangeListener(DatabaseChangeListener l) {
515         changeListeners.remove(l);
516     }
517
518     /*
519     public void setCompleters(Hashtable autoCompleters) {
520         _autoCompleters = autoCompleters;
521
522         for (Iterator i=getKeySet().iterator(); i.hasNext();) {
523             BibtexEntry be = getEntryById((String)(i.next()));
524             be.addPropertyChangeListener(new FieldChangeListener
525                                          (autoCompleters, be));
526
527             Util.updateCompletersForEntry(autoCompleters, be);
528         }
529         }*/
530 }