002635ae2806b885feab5c3b7710d8fbcdb30231
[debian/jabref.git] / src / java / net / sf / jabref / BibtexEntry.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 package net.sf.jabref;
32
33 import net.sf.jabref.export.FieldFormatter;
34 import java.beans.PropertyChangeEvent;
35 import java.beans.PropertyVetoException;
36 import java.beans.VetoableChangeListener;
37 import java.beans.VetoableChangeSupport;
38
39 import java.util.*;
40 import java.io.*;
41
42
43 public class BibtexEntry
44 {
45     public final static String ID_FIELD = "id";
46     private String _id;
47     private BibtexEntryType _type;
48     private Map _fields = new HashMap();
49     VetoableChangeSupport _changeSupport = new VetoableChangeSupport(this);
50
51     // Search and grouping status is stored in boolean fields for quick reference:
52     private boolean searchHit, groupHit;
53
54     public BibtexEntry(String id)
55     {
56         this(id, BibtexEntryType.OTHER);
57     }
58
59     public BibtexEntry(String id, BibtexEntryType type)
60     {
61         if (id == null)
62         {
63             throw new NullPointerException("Every BibtexEntry must have an ID");
64         }
65
66         _id = id;
67         setType(type);
68     }
69
70     /**
71      * Returns an array describing the optional fields for this entry.
72      */
73     public String[] getOptionalFields()
74     {
75         return _type.getOptionalFields();
76     }
77
78     /**
79      * Returns an array describing the required fields for this entry.
80      */
81     public String[] getRequiredFields()
82     {
83         return _type.getRequiredFields();
84     }
85
86     /**
87      * Returns an array describing general fields.
88      */
89     public String[] getGeneralFields() {
90         return _type.getGeneralFields();
91     }
92
93     /**
94      * Returns an array containing the names of all fields that are
95      * set for this particular entry.
96      */
97     public Object[] getAllFields() {
98         return _fields.keySet().toArray();
99     }
100
101     /**
102      * Returns a string describing the required fields for this entry.
103      */
104     public String describeRequiredFields()
105     {
106         return _type.describeRequiredFields();
107     }
108
109     /**
110      * Returns true if this entry contains the fields it needs to be
111      * complete.
112      */
113     public boolean hasAllRequiredFields()
114     {
115         return _type.hasAllRequiredFields(this);
116     }
117
118     /**
119      * Returns this entry's type.
120      */
121     public BibtexEntryType getType()
122     {
123         return _type;
124     }
125
126     /**
127      * Sets this entry's type.
128      */
129     public void setType(BibtexEntryType type)
130     {
131         if (type == null)
132         {
133             throw new NullPointerException(
134                 "Every BibtexEntry must have a type.  Instead of null, use type OTHER");
135         }
136
137         _type = type;
138     }
139
140     /**
141      * Prompts the entry to call BibtexEntryType.getType(String) with
142      * its current type name as argument, and sets its type according
143      * to what is returned. This method is called when a user changes
144      * the type customization, to make sure all entries are set with
145      * current types.
146      * @return true if the entry could find a type, false if not (in
147      * this case the type will have been set to
148      * BibtexEntryType.TYPELESS).
149      */
150     public boolean updateType() {
151         BibtexEntryType newType = BibtexEntryType.getType(_type.getName());
152         if (newType != null) {
153             _type = newType;
154             return true;
155         }
156         _type = BibtexEntryType.TYPELESS;
157         return false;
158     }
159
160     /**
161      * Sets this entry's ID, provided the database containing it
162      * doesn't veto the change.
163      */
164     public void setId(String id) throws KeyCollisionException {
165
166         if (id == null) {
167             throw new
168                 NullPointerException("Every BibtexEntry must have an ID");
169         }
170
171         try
172         {
173             firePropertyChangedEvent(ID_FIELD, _id, id);
174         }
175         catch (PropertyVetoException pv)
176         {
177             throw new KeyCollisionException("Couldn't change ID: " + pv);
178         }
179
180         _id = id;
181     }
182
183     /**
184      * Returns this entry's ID.
185      */
186     public String getId()
187     {
188         return _id;
189     }
190
191     /**
192      * Returns the contents of the given field, or null if it is not set.
193      */
194     public Object getField(String name) {
195         return _fields.get(name);
196     }
197
198     public String getCiteKey() {
199         return (_fields.containsKey(BibtexFields.KEY_FIELD) ?
200                 (String)_fields.get(BibtexFields.KEY_FIELD) : null);
201     }
202
203     /**
204      * Sets a number of fields simultaneously. The given HashMap contains field
205      * names as keys, each mapped to the value to set.
206      * WARNING: this method does not notify change listeners, so it should *NOT*
207      * be used for entries that are being displayed in the GUI. Furthermore, it
208      * does not check values for content, so e.g. empty strings will be set as such.
209      */
210     public void setField(HashMap fields){
211         _fields.putAll(fields);
212     }
213
214     public void setField(String name, Object value) {
215
216         if (ID_FIELD.equals(name)) {
217             throw new IllegalArgumentException("The field name '" + name +
218                                                "' is reserved");
219         }
220
221         // This mechanism is probably not really necessary.
222         //Object normalValue = FieldTypes.normalize(name, value);
223
224
225         Object oldValue = _fields.get(name);
226
227         try {
228                 /* The first event is no longer needed, so the following comment doesn't apply
229                    as of 2005.08.11.
230
231             // First throw an empty event that just signals that this entry
232                 // is about to change. This is needed, so the EntrySorter can
233                 // remove the entry from its TreeSet. After a sort-sensitive
234                 // field changes, the entry will not be found by the TreeMap,
235                 // so without this event it would be impossible to reinsert this
236                 // entry to keep everything sorted properly.
237             firePropertyChangedEvent(null, null, null);
238             */
239
240             // We set the field before throwing the changeEvent, to enable
241                 // the change listener to access the new value if the change
242                 // sets off a change in database sorting etc.
243                 _fields.put(name, value);
244             firePropertyChangedEvent(name, oldValue, value);
245         } catch (PropertyVetoException pve) {
246                 // Since we have already made the change, we must undo it since
247                 // the change was rejected:
248                 _fields.put(name, oldValue);
249             throw new IllegalArgumentException("Change rejected: " + pve);
250         }
251
252     }
253
254     /**
255      * Removes the mapping for the field name.
256      */
257     public void clearField(String name) {
258
259       if (ID_FIELD.equals(name)) {
260            throw new IllegalArgumentException("The field name '" + name +
261                                               "' is reserved");
262        }
263        Object oldValue = _fields.get(name);
264        _fields.remove(name);
265        try {
266            firePropertyChangedEvent(name, oldValue, "");
267        } catch (PropertyVetoException pve) {
268            throw new IllegalArgumentException("Change rejected: " + pve);
269        }
270
271
272     }
273
274     protected boolean allFieldsPresent(String[] fields) {
275         for (int i = 0; i < fields.length; i++) {
276             if (getField(fields[i]) == null) {
277                 return false;
278             }
279         }
280
281         return true;
282     }
283
284     private void firePropertyChangedEvent(String fieldName, Object oldValue,
285         Object newValue) throws PropertyVetoException
286     {
287         _changeSupport.fireVetoableChange(new PropertyChangeEvent(this,
288                 fieldName, oldValue, newValue));
289     }
290
291     /**
292      * Adds a VetoableChangeListener, which is notified of field
293      * changes. This is useful for an object that needs to update
294      * itself each time a field changes.
295      */
296     public void addPropertyChangeListener(VetoableChangeListener listener)
297     {
298         _changeSupport.addVetoableChangeListener(listener);
299     }
300
301     /**
302      * Removes a property listener.
303      */
304     public void removePropertyChangeListener(VetoableChangeListener listener)
305     {
306         _changeSupport.removeVetoableChangeListener(listener);
307     }
308
309     /**
310      * Write this entry to the given Writer, with the given FieldFormatter.
311      * @param write True if this is a write, false if it is a display. The write will
312      * not include non-writeable fields if it is a write, otherwise non-displayable fields
313      * will be ignored. Refer to GUIGlobals for isWriteableField(String) and
314      * isDisplayableField(String).
315      */
316     public void write(Writer out, FieldFormatter ff, boolean write) throws IOException {
317         // Write header with type and bibtex-key.
318         out.write("@"+_type.getName().toUpperCase()+"{");
319
320         String str = Util.shaveString((String)getField(BibtexFields.KEY_FIELD));
321         out.write(((str == null) ? "" : str)+","+Globals.NEWLINE);
322         HashMap written = new HashMap();
323         written.put(BibtexFields.KEY_FIELD, null);
324         boolean hasWritten = false;
325         // Write required fields first.
326         String[] s = getRequiredFields();
327         if (s != null) for (int i=0; i<s.length; i++) {
328             hasWritten = hasWritten | writeField(s[i], out, ff, hasWritten);
329             written.put(s[i], null);
330         }
331         // Then optional fields.
332         s = getOptionalFields();
333         if (s != null) for (int i=0; i<s.length; i++) {
334             if (!written.containsKey(s[i])) { // If field appears both in req. and opt. don't repeat.
335                 //writeField(s[i], out, ff);
336                 hasWritten = hasWritten | writeField(s[i], out, ff, hasWritten);
337                 written.put(s[i], null);
338             }
339         }
340         // Then write remaining fields in alphabetic order.
341         TreeSet remainingFields = new TreeSet();
342         for (Iterator i = _fields.keySet().iterator(); i.hasNext(); ) {
343             String key = (String)i.next();
344             boolean writeIt = (write ? BibtexFields.isWriteableField(key) :
345                                BibtexFields.isDisplayableField(key));
346             if (!written.containsKey(key) && writeIt)
347                        remainingFields.add(key);
348         }
349         for (Iterator i = remainingFields.iterator(); i.hasNext(); )
350             hasWritten = hasWritten | writeField((String)i.next(), out, ff, hasWritten);
351             //writeField((String)i.next(),out,ff);
352
353         // Finally, end the entry.
354         out.write((hasWritten ? Globals.NEWLINE : "")+"}"+Globals.NEWLINE);
355     }
356
357     /**
358      * Write a single field, if it has any content.
359      * @param name The field name
360      * @param out The Writer to send it to
361      * @param ff A formatter to filter field contents before writing
362      * @param isFirst Indicates whether this is the first field written for
363      *    this entry - if not, start by writing a comma and newline
364      * @return true if this field was written, false if it was skipped because
365      *    it was not set
366      * @throws IOException In case of an IO error
367      */
368     private boolean writeField(String name, Writer out,
369                             FieldFormatter ff, boolean isFirst) throws IOException {
370         Object o = getField(name);
371         if (o != null) {
372             if (isFirst)
373                 out.write(","+Globals.NEWLINE);
374             out.write("  "+name+" = ");
375
376             try {
377                 out.write(ff.format(o.toString(), name));
378             } catch (Throwable ex) {
379                 throw new IOException
380                     (Globals.lang("Error in field")+" '"+name+"': "+ex.getMessage());
381             }
382             return true;
383             //Util.writeField(name, o, out);
384             //out.write(","+Globals.NEWLINE);
385         } else
386             return false;
387     }
388
389     /**
390      * Returns a clone of this entry. Useful for copying.
391      */
392     public Object clone() {
393         BibtexEntry clone = new BibtexEntry(_id, _type);
394         clone._fields = (Map)((HashMap)_fields).clone();
395         return clone;
396     }
397
398
399     public String toString() {
400         return getType().getName()+":"+getField(BibtexFields.KEY_FIELD);
401     }
402
403     public boolean isSearchHit() {
404         return searchHit;
405     }
406
407     public void setSearchHit(boolean searchHit) {
408         this.searchHit = searchHit;
409     }
410
411     public boolean isGroupHit() {
412         return groupHit;
413     }
414
415     public void setGroupHit(boolean groupHit) {
416         this.groupHit = groupHit;
417     }
418
419     /**
420      * @param maxCharacters The maximum number of characters (additional
421      * characters are replaced with "..."). Set to 0 to disable truncation.
422      * @return A short textual description of the entry in the format:
423      * Author1, Author2: Title (Year)
424      */
425     public String getAuthorTitleYear(int maxCharacters) {
426         String[] s = new String[] {
427                 (String) getField("author"),
428                 (String) getField("title"),
429                 (String) getField("year")};
430         for (int i = 0; i < s.length; ++i)
431             if (s[i] == null)
432                 s[i] = "N/A";
433         String text = s[0] + ": \"" + s[1] + "\" (" + s[2] + ")";
434         if (maxCharacters <= 0 || text.length() <= maxCharacters)
435             return text;
436         return text.substring(0, maxCharacters + 1) + "...";
437     }
438 }