61a621b718289d59257f85986a5e4a37c5b3cb36
[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(BibtexFields.KEY_FIELD, key);
258         else
259           entry.clearField(BibtexFields.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      * Resolves any references to strings contained in this database,
338      * if possible.
339      */
340     public String resolveForStrings(String content) {
341         return resolveContent(content, new HashSet());
342     }
343
344     /**
345     * If the label represents a string contained in this database, returns
346     * that string's content. Resolves references to other strings, taking
347     * care not to follow a circular reference pattern.
348     * If the string is undefined, returns the label itself.
349     */
350     private String resolveString(String label, HashSet usedIds) {
351         for (java.util.Iterator i=_strings.keySet().iterator(); i.hasNext();) {
352             BibtexString string = (BibtexString)_strings.get(i.next());
353
354                 //Util.pr(label+" : "+string.getName());
355             if (string.getName().toLowerCase().equals(label.toLowerCase())) {
356
357                 // First check if this string label has been resolved
358                 // earlier in this recursion. If so, we have a
359                 // circular reference, and have to stop to avoid
360                 // infinite recursion.
361                 if (usedIds.contains(string.getId())) {
362                     Util.pr("Stopped due to circular reference in strings: "+label);
363                     return label;
364                 }
365                 // If not, log this string's ID now.
366                 usedIds.add(string.getId());
367
368                 // Ok, we found the string. Now we must make sure we
369                 // resolve any references to other strings in this one.
370                 String res = string.getContent();
371                 res = resolveContent(res, usedIds);
372
373                 // Finished with recursing this branch, so we remove our
374                 // ID again:
375                 usedIds.remove(string.getId());
376
377                 return res;
378             }
379         }
380
381         // If we get to this point, the string has obviously not been defined locally.
382         // Check if one of the standard BibTeX month strings has been used:
383         Object o;
384         if ((o = Globals.MONTH_STRINGS.get(label.toLowerCase())) != null) {
385             return (String)o;
386         }
387
388         return label;
389     }
390
391     private String resolveContent(String res, HashSet usedIds) {
392         //if (res.matches(".*#[-\\^\\:\\w]+#.*")) {
393     if (res.matches(".*#[^#]+#.*")) {
394             StringBuffer newRes = new StringBuffer();
395             int piv = 0, next = 0;
396             while ((next=res.indexOf("#", piv)) >= 0) {
397
398                 // We found the next string ref. Append the text
399                 // up to it.
400                 if (next > 0)
401                     newRes.append(res.substring(piv, next));
402                 int stringEnd = res.indexOf("#", next+1);
403                 if (stringEnd >= 0) {
404                     // We found the boundaries of the string ref,
405                     // now resolve that one.
406                     String refLabel = res.substring(next+1, stringEnd);
407                     String resolved = resolveString(refLabel, usedIds);
408                     if (refLabel.equals(resolved)) {
409                         // We got just the label in return, so this may not have
410                         // been intended as a string label, or it may be a label for
411                         // an undefined string. Therefore we prefer to display the #
412                         // characters rather than removing them:
413                         newRes.append(res.substring(next, stringEnd+1));
414                     } else
415                         // The string was resolved, so we display its meaning only,
416                         // stripping the # characters signifying the string label:
417                         newRes.append(resolved);
418                     piv = stringEnd+1;
419                 } else {
420                     // We didn't find the boundaries of the string ref. This
421                     // makes it impossible to interpret it as a string label.
422                     // So we should just append the rest of the text and finish.
423                     newRes.append(res.substring(next));
424                     piv = res.length();
425                     break;
426                 }
427
428             }
429             if (piv < res.length()-1)
430                 newRes.append(res.substring(piv));
431             res = newRes.toString();
432         }
433         return res;
434     }
435
436     //##########################################
437     //  usage:
438     //  isDuplicate=checkForDuplicateKeyAndAdd( null, b.getKey() , issueDuplicateWarning);
439     //############################################
440         // if the newkey already exists and is not the same as oldkey it will give a warning
441     // else it will add the newkey to the to set and remove the oldkey
442     public boolean checkForDuplicateKeyAndAdd(String oldKey, String newKey, boolean issueWarning){
443                 // Globals.logger(" checkForDuplicateKeyAndAdd [oldKey = " + oldKey + "] [newKey = " + newKey + "]");
444
445         boolean duplicate=false;
446         if(oldKey==null){// this is a new entry so don't bother removing oldKey
447             duplicate= addKeyToSet( newKey);
448         }else{
449             if(oldKey.equals(newKey)){// were OK because the user did not change keys
450                 duplicate=false;
451             }else{// user changed the key
452
453                 // removed the oldkey
454                 // But what if more than two have the same key?
455                 // this means that user can add another key and would not get a warning!
456                 // consider this: i add a key xxx, then i add another key xxx . I get a warning. I delete the key xxx. JBM
457                 // removes this key from the allKey. then I add another key xxx. I don't get a warning!
458                 // i need a way to count the number of keys of each type
459                 // hashmap=>int (increment each time)
460
461                 removeKeyFromSet( oldKey);
462                 duplicate = addKeyToSet( newKey );
463             }
464         }
465         if(duplicate==true && issueWarning==true){
466             JOptionPane.showMessageDialog(null,  Globals.lang("Warning there is a duplicate key")+":" + newKey ,
467                                           Globals.lang("Duplicate Key Warning"),
468                                           JOptionPane.WARNING_MESSAGE);//, options);
469
470         }
471         return duplicate;
472     }
473
474     /**
475      * Returns the number of occurences of the given key in this database.
476      */
477     public int getNumberOfKeyOccurences(String key) {
478         Object o = allKeys.get(key);
479         if (o == null)
480             return 0;
481         else
482             return ((Integer)o).intValue();
483
484     }
485
486     //========================================================
487     // keep track of all the keys to warn if there are duplicates
488     //========================================================
489     private boolean addKeyToSet(String key){
490                 boolean exists=false;
491                 if((key == null) || key.equals(""))
492                         return false;//don't put empty key
493                 if(allKeys.containsKey(key)){
494                         // warning
495                         exists=true;
496                         allKeys.put( key, new Integer( ((Integer)allKeys.get(key)).intValue() + 1));// incrementInteger( allKeys.get(key)));
497                 }else
498                         allKeys.put( key, new Integer(1));
499                 return exists;
500     }
501     //========================================================
502     // reduce the number of keys by 1. if this number goes to zero then remove from the set
503     // note: there is a good reason why we should not use a hashset but use hashmap instead
504     //========================================================
505     private void removeKeyFromSet(String key){
506                 if((key == null) || key.equals("")) return;
507                 if(allKeys.containsKey(key)){
508                         Integer tI = (Integer)allKeys.get(key); // if(allKeys.get(key) instanceof Integer)
509                         if(tI.intValue()==1)
510                                 allKeys.remove( key);
511                         else
512                                 allKeys.put( key, new Integer( ((Integer)tI).intValue() - 1));//decrementInteger( tI ));
513                 }
514     }
515
516
517
518     public void fireDatabaseChanged(DatabaseChangeEvent e) {
519         for (Iterator i=changeListeners.iterator(); i.hasNext();) {
520             ((DatabaseChangeListener)i.next()).databaseChanged(e);
521         }
522     }
523
524     public void addDatabaseChangeListener(DatabaseChangeListener l) {
525         changeListeners.add(l);
526     }
527
528     public void removeDatabaseChangeListener(DatabaseChangeListener l) {
529         changeListeners.remove(l);
530     }
531
532     /*
533     public void setCompleters(Hashtable autoCompleters) {
534         _autoCompleters = autoCompleters;
535
536         for (Iterator i=getKeySet().iterator(); i.hasNext();) {
537             BibtexEntry be = getEntryById((String)(i.next()));
538             be.addPropertyChangeListener(new FieldChangeListener
539                                          (autoCompleters, be));
540
541             Util.updateCompletersForEntry(autoCompleters, be);
542         }
543         }*/
544 }