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