package org.jabref.model.entry;
import java.util.Objects;
import java.util.Optional;
import org.jabref.model.strings.StringUtil;
/**
* This is an immutable class that keeps information regarding single
* author. It is just a container for the information, with very simple
* methods to access it.
* <p>
* Current usage: only methods <code>getLastOnly</code>,
* <code>getFirstLast</code>, and <code>getLastFirst</code> are used;
* all other methods are provided for completeness.
*/
public class Author {
private final String firstPart;
private final String firstAbbr;
private final String vonPart;
private final String lastPart;
private final String jrPart;
/**
* Creates the Author object. If any part of the name is absent, <CODE>null</CODE>
* must be passed; otherwise other methods may return erroneous results.
*
* @param first the first name of the author (may consist of several
* tokens, like "Charles Louis Xavier Joseph" in "Charles
* Louis Xavier Joseph de la Vall{\'e}e Poussin")
* @param firstabbr the abbreviated first name of the author (may consist of
* several tokens, like "C. L. X. J." in "Charles Louis
* Xavier Joseph de la Vall{\'e}e Poussin"). It is a
* responsibility of the caller to create a reasonable
* abbreviation of the first name.
* @param von the von part of the author's name (may consist of several
* tokens, like "de la" in "Charles Louis Xavier Joseph de la
* Vall{\'e}e Poussin")
* @param last the last name of the author (may consist of several
* tokens, like "Vall{\'e}e Poussin" in "Charles Louis Xavier
* Joseph de la Vall{\'e}e Poussin")
* @param jr the junior part of the author's name (may consist of
* several tokens, like "Jr. III" in "Smith, Jr. III, John")
*/
public Author(String first, String firstabbr, String von, String last, String jr) {
firstPart = addDotIfAbbreviation(removeStartAndEndBraces(first));
firstAbbr = removeStartAndEndBraces(firstabbr);
vonPart = removeStartAndEndBraces(von);
lastPart = removeStartAndEndBraces(last);
jrPart = removeStartAndEndBraces(jr);
}
public static String addDotIfAbbreviation(String name) {
// Avoid arrayindexoutof.... :
if ((name == null) || name.isEmpty()) {
return name;
}
// If only one character (uppercase letter), add a dot and return immediately:
if ((name.length() == 1) && Character.isLetter(name.charAt(0)) &&
Character.isUpperCase(name.charAt(0))) {
return name + ".";
}
StringBuilder sb = new StringBuilder();
char lastChar = name.charAt(0);
for (int i = 0; i < name.length(); i++) {
if (i > 0) {
lastChar = name.charAt(i - 1);
}
char currentChar = name.charAt(i);
sb.append(currentChar);
if (currentChar == '.') {
// A.A. -> A. A.
if (((i + 1) < name.length()) && Character.isUpperCase(name.charAt(i + 1))) {
sb.append(' ');
}
}
boolean currentIsUppercaseLetter = Character.isLetter(currentChar) && Character.isUpperCase(currentChar);
if (!currentIsUppercaseLetter) {
// No uppercase letter, hence nothing to do
continue;
}
boolean lastIsLowercaseLetter = Character.isLetter(lastChar) && Character.isLowerCase(lastChar);
if (lastIsLowercaseLetter) {
// previous character was lowercase (probably an acronym like JabRef) -> don't change anything
continue;
}
if ((i + 1) >= name.length()) {
// Current character is last character in input, so append dot
sb.append('.');
continue;
}
char nextChar = name.charAt(i + 1);
if ('-' == nextChar) {
// A-A -> A.-A.
sb.append(".");
continue;
}
if ('.' == nextChar) {
// Dot already there, so nothing to do
continue;
}
// AA -> A. A.
// Only append ". " if the rest of the 'word' is uppercase
boolean nextWordIsUppercase = true;
for (int j = i + 1; j < name.length(); j++) {
char furtherChar = name.charAt(j);
if (Character.isWhitespace(furtherChar) || (furtherChar == '-') || (furtherChar == '~')
|| (furtherChar == '.')) {
// end of word
break;
}
boolean furtherIsUppercaseLetter = Character.isLetter(furtherChar)
&& Character.isUpperCase(furtherChar);
if (!furtherIsUppercaseLetter) {
nextWordIsUppercase = false;
break;
}
}
if (nextWordIsUppercase) {
sb.append(". ");
}
}
return sb.toString().trim();
}
@Override
public int hashCode() {
return Objects.hash(firstAbbr, firstPart, jrPart, lastPart, vonPart);
}
/**
* Compare this object with the given one.
* <p>
* Will return true iff the other object is an Author and all fields are identical on a string comparison.
*/
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other instanceof Author) {
Author that = (Author) other;
return Objects.equals(firstPart, that.firstPart) && Objects.equals(firstAbbr, that.firstAbbr)
&& Objects.equals(
vonPart, that.vonPart)
&& Objects.equals(lastPart, that.lastPart) && Objects.equals(jrPart, that.jrPart);
}
return false;
}
/**
* @return true if the brackets in s are properly paired
*/
private boolean properBrackets(String s) {
// nested construct is there, check for "proper" nesting
int i = 0;
int level = 0;
while (i < s.length()) {
char c = s.charAt(i);
switch (c) {
case '{':
level++;
break;
case '}':
level--;
if (level == -1) { // improper nesting
return false;
}
break;
default:
break;
}
i++;
}
return level == 0;
}
/**
* Removes start and end brace at a string
* <p>
* E.g.,
* * {Vall{\'e}e Poussin} -> Vall{\'e}e Poussin
* * {Vall{\'e}e} {Poussin} -> Vall{\'e}e Poussin
* * Vall{\'e}e Poussin -> Vall{\'e}e Poussin
*/
private String removeStartAndEndBraces(String name) {
if (StringUtil.isBlank(name)) {
return null;
}
if (!name.contains("{")) {
return name;
}
String[] split = name.split(" ");
StringBuilder b = new StringBuilder();
for (String s : split) {
if ((s.length() > 2) && s.startsWith("{") && s.endsWith("}")) {
// quick solution (which we don't do: just remove first "{" and last "}"
// however, it might be that s is like {A}bbb{c}, where braces may not be removed
// inner
String inner = s.substring(1, s.length() - 1);
if (inner.contains("}")) {
if (properBrackets(inner)) {
s = inner;
}
} else {
// no inner curly brackets found, no check needed, inner can just be used as s
s = inner;
}
}
b.append(s).append(' ');
}
// delete last
b.deleteCharAt(b.length() - 1);
// now, all inner words are cleared
// case {word word word} remains
// as above, we have to be aware of {w}ord word wor{d} and {{w}ord word word}
String newName = b.toString();
if (newName.startsWith("{") && newName.endsWith("}")) {
String inner = newName.substring(1, newName.length() - 1);
if (properBrackets(inner)) {
return inner;
} else {
return newName;
}
} else {
return newName;
}
}
/**
* Returns the first name of the author stored in this object ("First").
*
* @return first name of the author (may consist of several tokens)
*/
public Optional<String> getFirst() {
return Optional.ofNullable(firstPart);
}
/**
* Returns the abbreviated first name of the author stored in this
* object ("F.").
*
* @return abbreviated first name of the author (may consist of several
* tokens)
*/
public Optional<String> getFirstAbbr() {
return Optional.ofNullable(firstAbbr);
}
/**
* Returns the von part of the author's name stored in this object
* ("von").
*
* @return von part of the author's name (may consist of several tokens)
*/
public Optional<String> getVon() {
return Optional.ofNullable(vonPart);
}
/**
* Returns the last name of the author stored in this object ("Last").
*
* @return last name of the author (may consist of several tokens)
*/
public Optional<String> getLast() {
return Optional.ofNullable(lastPart);
}
/**
* Returns the junior part of the author's name stored in this object
* ("Jr").
*
* @return junior part of the author's name (may consist of several
* tokens) or null if the author does not have a Jr. Part
*/
public Optional<String> getJr() {
return Optional.ofNullable(jrPart);
}
/**
* Returns von-part followed by last name ("von Last"). If both fields
* were specified as <CODE>null</CODE>, the empty string <CODE>""</CODE>
* is returned.
*
* @return 'von Last'
*/
public String getLastOnly() {
if (vonPart == null) {
return getLast().orElse("");
} else {
return lastPart == null ? vonPart : vonPart + ' ' + lastPart;
}
}
/**
* Returns the author's name in form 'von Last, Jr., First' with the
* first name full or abbreviated depending on parameter.
*
* @param abbr <CODE>true</CODE> - abbreviate first name, <CODE>false</CODE> -
* do not abbreviate
* @return 'von Last, Jr., First' (if <CODE>abbr==false</CODE>) or
* 'von Last, Jr., F.' (if <CODE>abbr==true</CODE>)
*/
public String getLastFirst(boolean abbr) {
StringBuilder res = new StringBuilder(getLastOnly());
getJr().ifPresent(jr -> res.append(", ").append(jr));
if (abbr) {
getFirstAbbr().ifPresent(firstA -> res.append(", ").append(firstA));
} else {
getFirst().ifPresent(first -> res.append(", ").append(first));
}
return res.toString();
}
/**
* Returns the author's name in form 'First von Last, Jr.' with the
* first name full or abbreviated depending on parameter.
*
* @param abbr <CODE>true</CODE> - abbreviate first name, <CODE>false</CODE> -
* do not abbreviate
* @return 'First von Last, Jr.' (if <CODE>abbr==false</CODE>) or 'F.
* von Last, Jr.' (if <CODE>abbr==true</CODE>)
*/
public String getFirstLast(boolean abbr) {
StringBuilder res = new StringBuilder();
if (abbr) {
getFirstAbbr().map(firstA -> firstA + ' ').ifPresent(res::append);
} else {
getFirst().map(first -> first + ' ').ifPresent(res::append);
}
res.append(getLastOnly());
getJr().ifPresent(jr -> res.append(", ").append(jr));
return res.toString();
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Author{");
sb.append("firstPart='").append(firstPart).append('\'');
sb.append(", firstAbbr='").append(firstAbbr).append('\'');
sb.append(", vonPart='").append(vonPart).append('\'');
sb.append(", lastPart='").append(lastPart).append('\'');
sb.append(", jrPart='").append(jrPart).append('\'');
sb.append('}');
return sb.toString();
}
/**
* Returns the name as "Last, Jr, F." omitting the von-part and removing
* starting braces.
*
* @return "Last, Jr, F." as described above or "" if all these parts
* are empty.
*/
public String getNameForAlphabetization() {
StringBuilder res = new StringBuilder();
getLast().ifPresent(res::append);
getJr().ifPresent(jr -> res.append(", ").append(jr));
getFirstAbbr().ifPresent(firstA -> res.append(", ").append(firstA));
while ((res.length() > 0) && (res.charAt(0) == '{')) {
res.deleteCharAt(0);
}
return res.toString();
}
}