package org.jabref.model.pdf;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.Optional;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
public class FileAnnotation {
private final static int ABBREVIATED_ANNOTATION_NAME_LENGTH = 45;
private static final String DATE_TIME_STRING = "^D:\\d{14}$";
private static final String DATE_TIME_STRING_WITH_TIME_ZONE = "^D:\\d{14}\\+.+";
private static final String ANNOTATION_DATE_FORMAT = "yyyyMMddHHmmss";
private final String author;
private final LocalDateTime timeModified;
private final int page;
private final String content;
private final FileAnnotationType annotationType;
private final Optional<FileAnnotation> linkedFileAnnotation;
/**
* A flexible constructor, mainly used as dummy if there is actually no annotation.
*
* @param author The authors of the annotation
* @param timeModified The last time this annotation was modified
* @param pageNumber The page of the pdf where the annotation occurs
* @param content the actual content of the annotation
* @param annotationType the type of the annotation
*/
public FileAnnotation(final String author, final LocalDateTime timeModified, final int pageNumber,
final String content, final FileAnnotationType annotationType, final Optional<FileAnnotation> linkedFileAnnotation) {
this.author = author;
this.timeModified = timeModified;
this.page = pageNumber;
this.content = parseContent(content);
this.annotationType = annotationType;
this.linkedFileAnnotation = linkedFileAnnotation;
}
/**
* Creating a normal FileAnnotation from a PDAnnotation.
*
* @param annotation The actual annotation that holds the information
* @param pageNumber The page of the pdf where the annotation occurs
*/
public FileAnnotation(final PDAnnotation annotation, final int pageNumber) {
this(annotation.getDictionary().getString(COSName.T),
extractModifiedTime(annotation.getModifiedDate()),
pageNumber, annotation.getContents(), FileAnnotationType.parse(annotation), Optional.empty());
}
/**
* For creating a FileAnnotation that has a connection to another FileAnnotation. Needed when creating a text
* highlighted or underlined annotation with a sticky note.
*
* @param annotation The actual annotation that holds the information
* @param pageNumber The page of the pdf where the annotation occurs
* @param linkedFileAnnotation The corresponding note of a marked text area.
*/
public FileAnnotation(final PDAnnotation annotation, final int pageNumber, FileAnnotation linkedFileAnnotation) {
this(annotation.getDictionary().getString(COSName.T), extractModifiedTime(annotation.getModifiedDate()),
pageNumber, annotation.getContents(), FileAnnotationType.parse(annotation), Optional.of(linkedFileAnnotation));
}
/**
* Parses a String into a LocalDateTime.
*
* @param dateTimeString In this case of format yyyyMMddHHmmss.
* @return a LocalDateTime parsed from the dateTimeString
*/
public static LocalDateTime extractModifiedTime(String dateTimeString) {
if (dateTimeString == null) {
return LocalDateTime.now();
}
if (dateTimeString.matches(DATE_TIME_STRING_WITH_TIME_ZONE)) {
dateTimeString = dateTimeString.substring(2, 16);
} else if (dateTimeString.matches(DATE_TIME_STRING)) {
dateTimeString = dateTimeString.substring(2);
}
return LocalDateTime.parse(dateTimeString, DateTimeFormatter.ofPattern(ANNOTATION_DATE_FORMAT));
}
private String parseContent(final String content) {
if (content == null) {
return "";
}
final String unreadableContent = "þÿ";
if (content.trim().equals(unreadableContent)) {
return "";
}
return content.trim();
}
/**
* Abbreviate annotation names when they are longer than {@code ABBREVIATED_ANNOTATION_NAME_LENGTH} chars
*
* @param annotationName annotation to be shortened
* @return the abbreviated name
*/
private String abbreviateAnnotationName(final String annotationName) {
if (annotationName.length() > ABBREVIATED_ANNOTATION_NAME_LENGTH) {
return annotationName.subSequence(0, ABBREVIATED_ANNOTATION_NAME_LENGTH).toString() + "...";
}
return annotationName;
}
@Override
public String toString() {
return abbreviateAnnotationName(content);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if ((other == null) || (getClass() != other.getClass())) {
return false;
}
FileAnnotation that = (FileAnnotation) other;
return Objects.equals(this.annotationType, that.annotationType)
&& Objects.equals(this.author, that.author)
&& Objects.equals(this.content, that.content)
&& Objects.equals(this.page, that.page)
&& Objects.equals(this.linkedFileAnnotation, that.linkedFileAnnotation)
&& Objects.equals(this.timeModified, that.timeModified);
}
@Override
public int hashCode() {
return Objects.hash(annotationType, author, content, page, linkedFileAnnotation, timeModified);
}
public String getAuthor() {
return author;
}
public LocalDateTime getTimeModified() {
return timeModified;
}
public int getPage() {
return page;
}
public String getContent() {
return content;
}
public FileAnnotationType getAnnotationType() {
return annotationType;
}
public boolean hasLinkedAnnotation() {
return this.linkedFileAnnotation.isPresent();
}
/**
* Before this getter is called the presence of the linked annotation must be checked via hasLinkedAnnotation()!
*
* @return the note attached to the annotation
*/
public FileAnnotation getLinkedFileAnnotation() {
return linkedFileAnnotation.get();
}
}