package cgeo.geocaching.log; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.HashCodeBuilder; import android.support.annotation.NonNull; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.regex.Pattern; import cgeo.geocaching.CgeoApplication; import cgeo.geocaching.R; import cgeo.geocaching.models.Image; import cgeo.geocaching.settings.Settings; import cgeo.geocaching.utils.HtmlUtils; import cgeo.geocaching.utils.MatcherWrapper; /** * Entry in a log book. * * {@link LogEntry} Objects are immutable. They should be manipulated by {@link LogEntry.Builder}. Use {@link LogEntry#buildUpon()} * to create a {@link LogEntry.Builder} object capable of creating a new {@link LogEntry}. * This object should not be referenced directly from a Geocache object to reduce the memory usage * of the Geocache objects. * */ public final class LogEntry { private static final Pattern PATTERN_REMOVE_COLORS = Pattern.compile("</?font.*?>", Pattern.CASE_INSENSITIVE); /** Log id */ public final int id; /** The {@link LogType} */ @NonNull private final LogType logType; /** The author */ @NonNull public final String author; /** The log message */ @NonNull public final String log; /** The log date */ public final long date; /** Is a found log */ public final int found; /** Friend's log entry */ public final boolean friend; /** log {@link Image} List */ @NonNull private final List<Image> logImages; /** Spotted cache name */ @NonNull public final String cacheName; // used for trackables /** Spotted cache guid */ @NonNull public final String cacheGuid; // used for trackables /** Spotted cache geocode */ @NonNull public final String cacheGeocode; // used for trackables /** * Helper class for building or manipulating {@link LogEntry} references. * * Use {@link #buildUpon()} to obtain a builder representing an existing {@link LogEntry}. */ public static class Builder { /** Log id */ private int id; /** The LogType */ @NonNull private LogType logType; /** The author */ @NonNull private String author; /** The log message */ @NonNull private String message; /** The log date */ private long date; /** Is a found log */ private int found; /** Friend's log entry */ private boolean friend; /** log {@link Image} List */ private List<Image> logImages; /** Spotted cache name */ @NonNull private String cacheName; // used for trackables /** Spotted cache guid */ @NonNull private String cacheGuid; // used for trackables /** Spotted cache geocode */ @NonNull private String cacheGeocode; // used for trackables /** * Create a new LogEntry.Builder. * */ public Builder() { id = 0; logType = LogType.UNKNOWN; author = ""; message = ""; date = 0; found = -1; friend = false; logImages = null; cacheName = ""; cacheGuid = ""; cacheGeocode = ""; } /** * Build an immutable {@link LogEntry} Object. * */ @NonNull public LogEntry build() { final List<Image> finalLogImage = logImages == null ? Collections.<Image>emptyList() : logImages; return new LogEntry(id, logType, StringUtils.defaultIfBlank(author, Settings.getUserName()), message, date, found, friend, finalLogImage, cacheName, cacheGuid, cacheGeocode); } /** * Set {@link LogEntry} id. * * @param id * The log id */ @NonNull public Builder setId(final int id) { this.id = id; return this; } /** * Get {@link LogEntry} id. Throws an exception if {@link #setId(int)} has not be called previously. * * @return The log id */ public int getId() { if (id == 0) { throw new IllegalStateException("setId must be called before getId"); } return id; } /** * Set {@link LogEntry} {@link LogType}. * * @param logType * The {@link LogType} */ @NonNull public Builder setLogType(@NonNull final LogType logType) { this.logType = logType; return this; } /** * Set {@link LogEntry} author. * * @param author * The author */ @NonNull public Builder setAuthor(@NonNull final String author) { this.author = author; return this; } /** * Set {@link LogEntry} log message. * * @param message * The log message */ @NonNull public Builder setLog(@NonNull final String message) { this.message = HtmlUtils.removeExtraTags(message); return this; } /** * Set {@link LogEntry} date. * * @param date * The log date */ @NonNull public Builder setDate(final long date) { this.date = date; return this; } /** * Set {@link LogEntry} found. * * @param found * {@code true} if this is a found {@link LogType} */ @NonNull public Builder setFound(final int found) { this.found = found; return this; } /** * Set {@link LogEntry} friend. * * @param friend * {@code true} if this is a log from a friend */ @NonNull public Builder setFriend(final boolean friend) { this.friend = friend; return this; } /** * Set {@link LogEntry} spotted cache name. * * @param cacheName * The cache name */ @NonNull public Builder setCacheName(@NonNull final String cacheName) { this.cacheName = cacheName; return this; } /** * Set {@link LogEntry} spotted cache Guid. * * @param cacheGuid * The cache guid */ @NonNull public Builder setCacheGuid(@NonNull final String cacheGuid) { this.cacheGuid = cacheGuid; return this; } /** * Set {@link LogEntry} spotted cache Geocode. * * @param cacheGeocode * The cache geocode */ @NonNull public Builder setCacheGeocode(@NonNull final String cacheGeocode) { this.cacheGeocode = cacheGeocode; return this; } /** * Set {@link LogEntry} images. * * @param logImages * The {@code Image}s List */ @NonNull public Builder setLogImages(@NonNull final List<Image> logImages) { this.logImages = logImages; return this; } /** * Add a new {@link Image} to the {@link LogEntry}. * * @param image * to be added to the {@link LogEntry} */ public Builder addLogImage(final Image image) { if (image.equals(Image.NONE)) { return this; } if (logImages == null || logImages.isEmpty()) { logImages = new ArrayList<>(); } logImages.add(image); return this; } } /** * LogEntry main constructor. * * @param id log id * @param logType log {@link LogType} * @param author log author * @param log log message * @param date log date * @param found is a found log * @param friend is a friend log * @param logImages log images * @param cacheName spotted cache name * @param cacheGuid spotted cache guid * @param cacheGeocode spotted cache geocode */ private LogEntry(final int id, @NonNull final LogType logType, @NonNull final String author, @NonNull final String log, final long date, final int found, final boolean friend, @NonNull final List<Image> logImages, @NonNull final String cacheName, @NonNull final String cacheGuid, @NonNull final String cacheGeocode) { this.id = id; this.logType = logType; this.author = author; this.log = log; this.date = date; this.found = found; this.friend = friend; this.logImages = logImages; this.cacheName = cacheName; this.cacheGuid = cacheGuid; this.cacheGeocode = cacheGeocode; } /** * Constructs a new {@link LogEntry.Builder}, copying the attributes from this LogEntry. * * @return * A new {@link LogEntry.Builder} */ public Builder buildUpon() { return new Builder() .setId(id) .setLogType(logType) .setAuthor(author) .setLog(log) .setDate(date) .setFound(found) .setFriend(friend) .setLogImages(logImages) .setCacheName(cacheName) .setCacheGuid(cacheGuid) .setCacheGeocode(cacheGeocode); } /** * Get the {@link LogType} * * @return * The {@link LogType} */ public LogType getType() { return logType; } /** * Get the {@link Image} for log * * @return * The {@link Image}s List */ @NonNull public List<Image> getLogImages() { return logImages; } /** * Get the hashCode of a LogEntry Object. * * @return * The object's hash code */ @Override public int hashCode() { return new HashCodeBuilder() .append(id).append(logType).append(author).append(log).append(date).append(found) .append(friend).append(logImages).append(cacheName).append(cacheGuid).append(cacheGeocode) .build(); } /** * {@link LogEntry} {@link Comparator} by descending date */ public static final Comparator<LogEntry> DESCENDING_DATE_COMPARATOR = new Comparator<LogEntry>() { @Override public int compare(final LogEntry logEntry1, final LogEntry logEntry2) { return (int) (logEntry2.date - logEntry1.date); } }; /** * Return {@code true} if passed {@link LogType} Object is equal to the current {@link LogType} Object. * Object are also detected as equal if date, {@link LogType}, author and log are the same. * * @return * {@code true} if objects are identical */ @Override public boolean equals(final Object obj) { if (this == obj) { return true; } if (!(obj instanceof LogEntry)) { return false; } final LogEntry otherLog = (LogEntry) obj; return date == otherLog.date && logType == otherLog.logType && author.compareTo(otherLog.author) == 0 && log.compareTo(otherLog.log) == 0; } /** * Check if current LogType has Images. * Check if current {@link LogType} has {@link Image}. * * @return * {@code true} if {@link LogType} has images */ public boolean hasLogImages() { return CollectionUtils.isNotEmpty(logImages); } /** * Get the images titles separated by commas. * If no titles are present, display a 'default title' * * @return * {@link Image} titles separated by commas or 'default title' */ public CharSequence getImageTitles() { final List<String> titles = new ArrayList<>(5); assert logImages != null; // make compiler happy for (final Image image : logImages) { if (StringUtils.isNotBlank(image.getTitle())) { titles.add(HtmlUtils.extractText(image.getTitle())); } } if (titles.isEmpty()) { titles.add(CgeoApplication.getInstance().getString(R.string.cache_log_image_default_title)); } return "• " + StringUtils.join(titles, "\n• "); } /** * Get the log message to be displayed. Depending on the settings, color tags might be removed. * * @return * Log message */ public String getDisplayText() { if (Settings.getPlainLogs()) { final MatcherWrapper matcher = new MatcherWrapper(PATTERN_REMOVE_COLORS, log); return matcher.replaceAll(StringUtils.EMPTY); } return log; } /** * Check if the LogEntry is owned by the current configured user on geocaching.com connector. * * @return * {@code true} if LogEntry is from current user */ public boolean isOwn() { return author.equalsIgnoreCase(Settings.getUserName()); } }