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.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.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.TreeSet; import net.sf.jabref.model.FieldChange; import net.sf.jabref.model.database.BibDatabase; import net.sf.jabref.model.event.FieldChangedEvent; import com.google.common.base.Strings; import com.google.common.eventbus.EventBus; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class BibEntry implements Cloneable { private static final Log LOGGER = LogFactory.getLog(BibEntry.class); public static final String TYPE_HEADER = "entrytype"; public static final String KEY_FIELD = "bibtexkey"; protected static final String ID_FIELD = "id"; private static final String DEFAULT_TYPE = "misc"; private String id; private String type; private Map<String, String> fields = new HashMap<>(); /* * Map to store the words in every field */ private final Map<String, Set<String>> fieldsAsWords = new HashMap<>(); // Search and grouping status is stored in boolean fields for quick reference: private boolean searchHit; private boolean groupHit; private String parsedSerialization; /* * Marks whether the complete serialization, which was read from file, should be used. * * Is set to false, if parts of the entry change. This causes the entry to be serialized based on the internal state (and not based on the old serialization) */ private boolean changed; private final EventBus eventBus = new EventBus(); /** * Constructs a new BibEntry. The internal ID is set to IdGenerator.next() */ public BibEntry() { this(IdGenerator.next()); } /** * Constructs a new BibEntry with the given ID and DEFAULT_TYPE * * @param id The ID to be used */ public BibEntry(String id) { this(id, DEFAULT_TYPE); } /** * Constructs a new BibEntry with the given ID and given type * * @param id The ID to be used * @param type The type to set. May be null or empty. In that case, DEFAULT_TYPE is used. */ public BibEntry(String id, String type) { Objects.requireNonNull(id, "Every BibEntry must have an ID"); this.id = id; setType(type); } /** * Sets this entry's ID, provided the database containing it * doesn't veto the change. * * @param id The ID to be used */ public void setId(String id) { Objects.requireNonNull(id, "Every BibEntry must have an ID"); eventBus.post(new FieldChangedEvent(this, BibEntry.ID_FIELD, id)); this.id = id; changed = true; } /** * Returns this entry's ID. */ public String getId() { return id; } /** * Sets the cite key AKA citation key AKA BibTeX key. * * Note: This is <emph>not</emph> the internal Id of this entry. The internal Id is always present, whereas the BibTeX key might not be present. * * @param newCiteKey The cite key to set. Must not be null, may be empty to remove it. */ public void setCiteKey(String newCiteKey) { setField(KEY_FIELD, newCiteKey); } /** * Returns the cite key AKA citation key AKA BibTeX key, or null if it is not set. * * Note: this is <emph>not</emph> the internal Id of this entry. The internal Id is always present, whereas the BibTeX key might not be present. */ public String getCiteKey() { return fields.get(KEY_FIELD); } public boolean hasCiteKey() { return !Strings.isNullOrEmpty(getCiteKey()); } /** * Returns this entry's type. */ public String getType() { return type; } /** * Sets this entry's type. */ public void setType(String type) { String newType; if (Strings.isNullOrEmpty(type)) { newType = DEFAULT_TYPE; } else { newType = type; } // 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 = newType.toLowerCase(Locale.ENGLISH); changed = true; eventBus.post(new FieldChangedEvent(this, TYPE_HEADER, newType)); } /** * Sets this entry's type. */ public void setType(EntryType type) { this.setType(type.getName()); } /** * 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 the contents of the given field, or null if it is not set. */ @Deprecated //Use getFieldOptional instead public String getField(String name) { return fields.get(toLowerCase(name)); } /** * Returns the contents of the given field as an Optional. */ public Optional<String> getFieldOptional(String name) { return Optional.ofNullable(fields.get(toLowerCase(name))); } /** * Returns true if the entry has the given field, or false if it is not set. */ public boolean hasField(String name) { return fields.containsKey(toLowerCase(name)); } private String toLowerCase(String fieldName) { Objects.requireNonNull(fieldName, "field name must not be null"); return fieldName.toLowerCase(Locale.ENGLISH); } /** * 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 Optional<String> getFieldOrAlias(String name) { Optional<String> fieldValue = getFieldOptional(toLowerCase(name)); if (fieldValue.isPresent() && !fieldValue.get().isEmpty()) { return fieldValue; } // No value of this field found, so look at the alias String aliasForField = EntryConverter.FIELD_ALIASES.get(name); if (aliasForField != null) { return getFieldOptional(aliasForField); } // Finally, handle dates if (FieldName.DATE.equals(name)) { Optional<String> year = getFieldOptional(FieldName.YEAR); if (year.isPresent()) { MonthUtil.Month month = MonthUtil.getMonth(getFieldOptional(FieldName.MONTH).orElse("")); if (month.isValid()) { return Optional.of(year.get() + '-' + month.twoDigitNumber); } else { return year; } } } if (FieldName.YEAR.equals(name) || FieldName.MONTH.equals(name)) { Optional<String> date = getFieldOptional(FieldName.DATE); if (!date.isPresent()) { return Optional.empty(); } // 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.get()); Calendar calendar = Calendar.getInstance(); calendar.setTime(parsedDate); if (FieldName.YEAR.equals(name)) { return Optional.of(Integer.toString(calendar.get(Calendar.YEAR))); } if (FieldName.MONTH.equals(name)) { return Optional.of(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.get()); Calendar calendar = Calendar.getInstance(); calendar.setTime(parsedDate); if (FieldName.YEAR.equals(name)) { return Optional.of(Integer.toString(calendar.get(Calendar.YEAR))); } } catch (ParseException e2) { LOGGER.warn("Could not parse entry " + name, e2); return Optional.empty(); // Date field not in valid format } } } return Optional.empty(); } /** * Sets a number of fields simultaneously. The given HashMap contains field * names as keys, each mapped to the value to set. */ public void setField(Map<String, String> fields) { Objects.requireNonNull(fields, "fields must not be null"); fields.forEach(this::setField); } /** * Set a field, and notify listeners about the change. * @param name The field to set. * @param value The value to set. */ public Optional<FieldChange> setField(String name, String value) { Objects.requireNonNull(name, "field name must not be null"); Objects.requireNonNull(value, "field value must not be null"); String fieldName = toLowerCase(name); if (value.isEmpty()) { return clearField(fieldName); } String oldValue = getField(fieldName); if (value.equals(oldValue)) { return Optional.empty(); } if (BibEntry.ID_FIELD.equals(fieldName)) { throw new IllegalArgumentException("The field name '" + name + "' is reserved"); } changed = true; fields.put(fieldName, value); fieldsAsWords.remove(fieldName); FieldChange change = new FieldChange(this, fieldName, oldValue, value); eventBus.post(new FieldChangedEvent(change)); return Optional.of(change); } /** * Remove the mapping for the field name, and notify listeners about * the change. * * @param name The field to clear. */ public Optional<FieldChange> clearField(String name) { String fieldName = toLowerCase(name); if (BibEntry.ID_FIELD.equals(fieldName)) { throw new IllegalArgumentException("The field name '" + name + "' is reserved"); } Optional<String> oldValue = getFieldOptional(fieldName); if (!oldValue.isPresent()) { return Optional.empty(); } changed = true; fields.remove(fieldName); fieldsAsWords.remove(fieldName); FieldChange change = new FieldChange(this, fieldName, oldValue.get(), null); eventBus.post(new FieldChangedEvent(change)); return Optional.of(change); } /** * 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. */ public boolean allFieldsPresent(List<String> allFields, BibDatabase database) { final String orSeparator = "/"; for (String field : allFields) { String fieldName = toLowerCase(field); // OR fields if (fieldName.contains(orSeparator)) { String[] altFields = field.split(orSeparator); if (!atLeastOnePresent(altFields, database)) { return false; } } else { if (BibDatabase.getResolvedField(fieldName, this, database) == null) { return false; } } } return true; } private boolean atLeastOnePresent(String[] fieldsToCheck, BibDatabase database) { for (String field : fieldsToCheck) { String fieldName = toLowerCase(field); String value = BibDatabase.getResolvedField(fieldName, this, database); if ((value != null) && !value.isEmpty()) { return true; } } return false; } /** * Returns a clone of this entry. Useful for copying. */ @Override public Object clone() { BibEntry clone = new BibEntry(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 * <p> * 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[] { getFieldOptional(FieldName.AUTHOR).orElse("N/A"), getFieldOptional(FieldName.TITLE).orElse("N/A"), getFieldOptional(FieldName.YEAR).orElse("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. * * @return will return the publication date of the entry or null if no year was found. */ public Optional<String> getPublicationDate() { if (!hasField(FieldName.YEAR)) { return Optional.empty(); } Optional<String> year = getFieldOptional(FieldName.YEAR); Optional<String> monthString = getFieldOptional(FieldName.MONTH); if (monthString.isPresent()) { MonthUtil.Month month = MonthUtil.getMonth(monthString.get()); if (month.isValid()) { return Optional.of(year.orElse("") + "-" + month.twoDigitNumber); } } return year; } public void setParsedSerialization(String parsedSerialization) { changed = false; this.parsedSerialization = parsedSerialization; } public String getParsedSerialization() { return parsedSerialization; } public boolean hasChanged() { return changed; } public void setChanged(boolean changed) { this.changed = changed; } public Optional<FieldChange> putKeywords(Collection<String> keywords, String separator) { Objects.requireNonNull(keywords); Optional<String> oldValue = this.getFieldOptional(FieldName.KEYWORDS); if (keywords.isEmpty()) { // Clear keyword field if (oldValue.isPresent()) { return this.clearField(FieldName.KEYWORDS); } else { return Optional.empty(); } } // Set new keyword field String newValue = String.join(separator, keywords); return this.setField(FieldName.KEYWORDS, newValue); } /** * Check if a keyword already exists (case insensitive), if not: add it * * @param keyword Keyword to add */ public void addKeyword(String keyword, String separator) { Objects.requireNonNull(keyword, "keyword must not be null"); if (keyword.isEmpty()) { return; } Set<String> keywords = this.getKeywords(); keywords.add(keyword); this.putKeywords(keywords, separator); } /** * Add multiple keywords to entry * * @param keywords Keywords to add */ public void addKeywords(Collection<String> keywords, String separator) { Objects.requireNonNull(keywords); for (String keyword : keywords) { this.addKeyword(keyword, separator); } } public Set<String> getKeywords() { return net.sf.jabref.model.entry.EntryUtil.getSeparatedKeywords(this); } public Collection<String> getFieldValues() { return fields.values(); } public Map<String, String> getFieldMap() { return fields; } @Override public boolean equals(Object o) { if (this == o) { return true; } if ((o == null) || (getClass() != o.getClass())) { return false; } BibEntry entry = (BibEntry) o; return Objects.equals(type, entry.type) && Objects.equals(fields, entry.fields); } @Override public int hashCode() { return Objects.hash(type, fields); } public void registerListener(Object object) { this.eventBus.register(object); } public void unregisterListener(Object object) { this.eventBus.unregister(object); } public BibEntry withField(String field, String value) { setField(field, value); return this; } /* * Returns user comments (arbitrary text before the entry), if they exist. If not, returns the empty String */ public String getUserComments() { if (parsedSerialization != null) { try { // get the text before the entry String prolog = parsedSerialization.substring(0, parsedSerialization.indexOf('@')); // delete trailing whitespaces (between entry and text) prolog = prolog.replaceFirst("\\s+$", ""); // if there is any non whitespace text, write it if (prolog.length() > 0) { return prolog; } } catch (StringIndexOutOfBoundsException ignore) { // if this occurs a broken parsed serialization has been set, so just do nothing } } return ""; } public Set<String> getFieldAsWords(String field) { String fieldName = toLowerCase(field); Set<String> storedList = fieldsAsWords.get(fieldName); if (storedList != null) { return storedList; } else { String fieldValue = fields.get(fieldName); if (fieldValue == null) { return Collections.emptySet(); } else { HashSet<String> words = new HashSet<>(EntryUtil.getStringAsWords(fieldValue)); fieldsAsWords.put(fieldName, words); return words; } } } }