Java tutorial
/* Copyright (C) 2003-2015 JabRef contributors. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package net.sf.jabref.model.entry; import java.beans.PropertyChangeEvent; import java.beans.PropertyVetoException; import java.beans.VetoableChangeListener; import java.beans.VetoableChangeSupport; import java.text.DateFormat; import java.text.FieldPosition; import java.text.ParseException; import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.TreeSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.google.common.base.Strings; import net.sf.jabref.bibtex.EntryTypes; import net.sf.jabref.model.database.BibtexDatabase; public class BibtexEntry { private static final Log LOGGER = LogFactory.getLog(BibtexEntry.class); public static final String TYPE_HEADER = "entrytype"; public static final String KEY_FIELD = "bibtexkey"; private static final String ID_FIELD = "id"; private String id; private EntryType type; private Map<String, String> fields = new HashMap<>(); private final VetoableChangeSupport changeSupport = new VetoableChangeSupport(this); // Search and grouping status is stored in boolean fields for quick reference: private boolean searchHit; private boolean groupHit; public BibtexEntry() { this(IdGenerator.next()); } public BibtexEntry(String id) { this(id, EntryTypes.getBibtexEntryType("misc")); } public BibtexEntry(String id, EntryType type) { Objects.requireNonNull(id, "Every BibtexEntry must have an ID"); this.id = id; setType(type); } /** * @return An array describing the optional fields for this entry. "null" if no fields are required */ public List<String> getOptionalFields() { return type.getOptionalFields(); } /** * Returns all required field names. * No OR relationships are captured here. * * @return a List of required field name Strings */ public List<String> getRequiredFieldsFlat() { return type.getRequiredFieldsFlat(); } /** * Returns an set containing the names of all fields that are * set for this particular entry. * * @return a set of existing field names */ public Set<String> getFieldNames() { return new TreeSet<>(fields.keySet()); } /** * Returns true if this entry contains the fields it needs to be * complete. */ public boolean hasAllRequiredFields(BibtexDatabase database) { return allFieldsPresent(type.getRequiredFields(), database); } /** * Returns this entry's type. */ public EntryType getType() { return type; } /** * Sets this entry's type. */ public void setType(EntryType type) { Objects.requireNonNull(type, "Every BibtexEntry must have a type."); EntryType oldType = this.type; try { // We set the type before throwing the changeEvent, to enable // the change listener to access the new value if the change // sets off a change in database sorting etc. this.type = type; firePropertyChangedEvent(TYPE_HEADER, oldType != null ? oldType.getName() : null, type.getName()); } catch (PropertyVetoException pve) { pve.printStackTrace(); } } /** * Sets this entry's ID, provided the database containing it * doesn't veto the change. */ public void setId(String id) { Objects.requireNonNull(id, "Every BibtexEntry must have an ID"); try { firePropertyChangedEvent(BibtexEntry.ID_FIELD, this.id, id); } catch (PropertyVetoException pv) { throw new IllegalStateException("Couldn't change ID: " + pv); } this.id = id; } /** * Returns this entry's ID. */ public String getId() { return id; } /** * Returns the contents of the given field, or null if it is not set. */ public String getField(String name) { return fields.get(name); } /** * Returns the contents of the given field, its alias or null if both are * not set. * <p> * The following aliases are considered (old bibtex <-> new biblatex) based * on the BibLatex documentation, chapter 2.2.5: * address <-> location * annote <-> annotation * archiveprefix <-> eprinttype * journal <-> journaltitle * key <-> sortkey * pdf <-> file * primaryclass <-> eprintclass * school <-> institution * These work bidirectional. * <p> * Special attention is paid to dates: (see the BibLatex documentation, * chapter 2.3.8) * The fields 'year' and 'month' are used if the 'date' * field is empty. Conversely, getFieldOrAlias("year") also tries to * extract the year from the 'date' field (analogously for 'month'). */ public String getFieldOrAlias(String name) { String fieldValue = getField(name); if (!Strings.isNullOrEmpty(fieldValue)) { return fieldValue; } // No value of this field found, so look at the alias String aliasForField = EntryConverter.FIELD_ALIASES.get(name); if (aliasForField != null) { return getField(aliasForField); } // Finally, handle dates if (name.equals("date")) { String year = getField("year"); MonthUtil.Month month = MonthUtil.getMonth(getField("month")); if (year != null) { if (month.isValid()) { return year + '-' + month.twoDigitNumber; } else { return year; } } } if (name.equals("year") || name.equals("month")) { String date = getField("date"); if (date == null) { return null; } // Create date format matching dates with year and month DateFormat df = new DateFormat() { static final String FORMAT1 = "yyyy-MM-dd"; static final String FORMAT2 = "yyyy-MM"; final SimpleDateFormat sdf1 = new SimpleDateFormat(FORMAT1); final SimpleDateFormat sdf2 = new SimpleDateFormat(FORMAT2); @Override public StringBuffer format(Date dDate, StringBuffer toAppendTo, FieldPosition fieldPosition) { throw new UnsupportedOperationException(); } @Override public Date parse(String source, ParsePosition pos) { if ((source.length() - pos.getIndex()) == FORMAT1.length()) { return sdf1.parse(source, pos); } return sdf2.parse(source, pos); } }; try { Date parsedDate = df.parse(date); Calendar calendar = Calendar.getInstance(); calendar.setTime(parsedDate); if (name.equals("year")) { return Integer.toString(calendar.get(Calendar.YEAR)); } if (name.equals("month")) { return Integer.toString(calendar.get(Calendar.MONTH) + 1); // Shift by 1 since in this calendar Jan = 0 } } catch (ParseException e) { // So not a date with year and month, try just to parse years df = new SimpleDateFormat("yyyy"); try { Date parsedDate = df.parse(date); Calendar calendar = Calendar.getInstance(); calendar.setTime(parsedDate); if (name.equals("year")) { return Integer.toString(calendar.get(Calendar.YEAR)); } } catch (ParseException e2) { LOGGER.warn("Could not parse entry " + name, e2); return null; // Date field not in valid format } } } return null; } /** * Returns the bibtex key, or null if it is not set. */ public String getCiteKey() { return fields.get(KEY_FIELD); } public boolean hasCiteKey() { return !Strings.isNullOrEmpty(getCiteKey()); } /** * Sets a number of fields simultaneously. The given HashMap contains field * names as keys, each mapped to the value to set. * WARNING: this method does not notify change listeners, so it should *NOT* * be used for entries that are being displayed in the GUI. Furthermore, it * does not check values for content, so e.g. empty strings will be set as such. */ public void setField(Map<String, String> fields) { this.fields.putAll(fields); } /** * Set a field, and notify listeners about the change. * * @param name The field to set. * @param value The value to set. */ public void setField(String name, String value) { if (BibtexEntry.ID_FIELD.equals(name)) { throw new IllegalArgumentException("The field name '" + name + "' is reserved"); } String oldValue = fields.get(name); try { // We set the field before throwing the changeEvent, to enable // the change listener to access the new value if the change // sets off a change in database sorting etc. fields.put(name, value); firePropertyChangedEvent(name, oldValue, value); } catch (PropertyVetoException pve) { // Since we have already made the change, we must undo it since // the change was rejected: fields.put(name, oldValue); throw new IllegalArgumentException("Change rejected: " + pve); } } /** * Remove the mapping for the field name, and notify listeners about * the change. * * @param name The field to clear. */ public void clearField(String name) { if (BibtexEntry.ID_FIELD.equals(name)) { throw new IllegalArgumentException("The field name '" + name + "' is reserved"); } Object oldValue = fields.get(name); fields.remove(name); try { firePropertyChangedEvent(name, oldValue, null); } catch (PropertyVetoException pve) { throw new IllegalArgumentException("Change rejected: " + pve); } } /** * Determines whether this entry has all the given fields present. If a non-null * database argument is given, this method will try to look up missing fields in * entries linked by the "crossref" field, if any. * * @param allFields An array of field names to be checked. * @param database The database in which to look up crossref'd entries, if any. This * argument can be null, meaning that no attempt will be made to follow crossrefs. * @return true if all fields are set or could be resolved, false otherwise. */ boolean allFieldsPresent(String[] allFields, BibtexDatabase database) { final String orSeparator = "/"; for (String field : allFields) { // OR fields if (field.contains(orSeparator)) { String[] altFields = field.split(orSeparator); if (!atLeastOnePresent(altFields, database)) { return false; } } else { if (BibtexDatabase.getResolvedField(field, this, database) == null) { return false; } } } return true; } boolean allFieldsPresent(List<String> allFields, BibtexDatabase database) { return allFieldsPresent(allFields.toArray(new String[allFields.size()]), database); } private boolean atLeastOnePresent(String[] fieldsToCheck, BibtexDatabase database) { for (String field : fieldsToCheck) { String value = BibtexDatabase.getResolvedField(field, this, database); if ((value != null) && !value.isEmpty()) { return true; } } return false; } private void firePropertyChangedEvent(String fieldName, Object oldValue, Object newValue) throws PropertyVetoException { changeSupport.fireVetoableChange(new PropertyChangeEvent(this, fieldName, oldValue, newValue)); } /** * Adds a VetoableChangeListener, which is notified of field * changes. This is useful for an object that needs to update * itself each time a field changes. */ public void addPropertyChangeListener(VetoableChangeListener listener) { changeSupport.addVetoableChangeListener(listener); } /** * Removes a property listener. */ public void removePropertyChangeListener(VetoableChangeListener listener) { changeSupport.removeVetoableChangeListener(listener); } /** * Returns a clone of this entry. Useful for copying. */ @Override public Object clone() { BibtexEntry clone = new BibtexEntry(id, type); clone.fields = new HashMap<>(fields); return clone; } /** * This returns a canonical BibTeX serialization. Special characters such as "{" or "&" are NOT escaped, but written * as is * * Serializes all fields, even the JabRef internal ones. Does NOT serialize "KEY_FIELD" as field, but as key */ @Override public String toString() { return CanonicalBibtexEntry.getCanonicalRepresentation(this); } public boolean isSearchHit() { return searchHit; } public void setSearchHit(boolean searchHit) { this.searchHit = searchHit; } public boolean isGroupHit() { return groupHit; } public void setGroupHit(boolean groupHit) { this.groupHit = groupHit; } /** * @param maxCharacters The maximum number of characters (additional * characters are replaced with "..."). Set to 0 to disable truncation. * @return A short textual description of the entry in the format: * Author1, Author2: Title (Year) */ public String getAuthorTitleYear(int maxCharacters) { String[] s = new String[] { getField("author"), getField("title"), getField("year") }; for (int i = 0; i < s.length; ++i) { if (s[i] == null) { s[i] = "N/A"; } } String text = s[0] + ": \"" + s[1] + "\" (" + s[2] + ')'; if ((maxCharacters <= 0) || (text.length() <= maxCharacters)) { return text; } return text.substring(0, maxCharacters + 1) + "..."; } /** * Will return the publication date of the given bibtex entry conforming to ISO 8601, i.e. either YYYY or YYYY-MM. * * @param entry * @return will return the publication date of the entry or null if no year was found. */ public String getPublicationDate() { Object o = getField("year"); if (o == null) { return null; } String year = YearUtil.toFourDigitYear(o.toString()); o = getField("month"); if (o != null) { MonthUtil.Month month = MonthUtil.getMonth(o.toString()); if (month.isValid()) { return year + "-" + month.twoDigitNumber; } } return year; } public void putKeywords(List<String> keywords) { Objects.requireNonNull(keywords); // Set Keyword Field String oldValue = this.getField("keywords"); String newValue; if (!keywords.isEmpty()) { newValue = String.join(", ", keywords); } else { newValue = null; } if ((oldValue == null) && (newValue == null)) { return; } if ((oldValue == null) || !oldValue.equals(newValue)) { this.setField("keywords", newValue); } } /** * Check if a keyword already exists (case insensitive), if not: add it * * @param keyword Keyword to add */ public void addKeyword(String keyword) { List<String> keywords = this.getSeparatedKeywords(); Boolean duplicate = false; if ((keyword == null) || (keyword.length() == 0)) { return; } for (String key : keywords) { if (keyword.equalsIgnoreCase(key)) { duplicate = true; break; } } if (!duplicate) { keywords.add(keyword); this.putKeywords(keywords); } } /** * Add multiple keywords to entry * * @param keywords Keywords to add */ public void addKeywords(List<String> keywords) { if (keywords != null) { for (String keyword : keywords) { this.addKeyword(keyword); } } } public List<String> getSeparatedKeywords() { return net.sf.jabref.model.entry.EntryUtil.getSeparatedKeywords(this.getField("keywords")); } }