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