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