/*
* Sone - Core.java - Copyright © 2010–2020 David Roden
*
* 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 3 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, see .
*/
package net.pterodactylus.sone.core;
import static com.google.common.base.Optional.fromNullable;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.primitives.Longs.tryParse;
import static java.lang.String.format;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static net.pterodactylus.sone.data.AlbumKt.getAllImages;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.codahale.metrics.*;
import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidAlbumFound;
import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidImageFound;
import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidParentAlbumFound;
import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostFound;
import net.pterodactylus.sone.core.ConfigurationSoneParser.InvalidPostReplyFound;
import net.pterodactylus.sone.core.event.*;
import net.pterodactylus.sone.data.Album;
import net.pterodactylus.sone.data.Client;
import net.pterodactylus.sone.data.Image;
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.PostReply;
import net.pterodactylus.sone.data.Profile;
import net.pterodactylus.sone.data.Profile.Field;
import net.pterodactylus.sone.data.Reply;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.data.Sone.SoneStatus;
import net.pterodactylus.sone.data.SoneKt;
import net.pterodactylus.sone.data.SoneOptions.LoadExternalContent;
import net.pterodactylus.sone.data.TemporaryImage;
import net.pterodactylus.sone.database.AlbumBuilder;
import net.pterodactylus.sone.database.Database;
import net.pterodactylus.sone.database.DatabaseException;
import net.pterodactylus.sone.database.ImageBuilder;
import net.pterodactylus.sone.database.PostBuilder;
import net.pterodactylus.sone.database.PostProvider;
import net.pterodactylus.sone.database.PostReplyBuilder;
import net.pterodactylus.sone.database.PostReplyProvider;
import net.pterodactylus.sone.database.SoneBuilder;
import net.pterodactylus.sone.database.SoneProvider;
import net.pterodactylus.sone.freenet.wot.Identity;
import net.pterodactylus.sone.freenet.wot.IdentityManager;
import net.pterodactylus.sone.freenet.wot.OwnIdentity;
import net.pterodactylus.sone.freenet.wot.event.IdentityAddedEvent;
import net.pterodactylus.sone.freenet.wot.event.IdentityRemovedEvent;
import net.pterodactylus.sone.freenet.wot.event.IdentityUpdatedEvent;
import net.pterodactylus.sone.freenet.wot.event.OwnIdentityAddedEvent;
import net.pterodactylus.sone.freenet.wot.event.OwnIdentityRemovedEvent;
import net.pterodactylus.sone.main.SonePlugin;
import net.pterodactylus.util.config.Configuration;
import net.pterodactylus.util.config.ConfigurationException;
import net.pterodactylus.util.service.AbstractService;
import net.pterodactylus.util.thread.NamedThreadFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Stopwatch;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import kotlin.jvm.functions.Function1;
/**
* The Sone core.
*/
@Singleton
public class Core extends AbstractService implements SoneProvider, PostProvider, PostReplyProvider {
/** The logger. */
private static final Logger logger = getLogger(Core.class.getName());
/** The start time. */
private final long startupTime = System.currentTimeMillis();
private final AtomicBoolean debug = new AtomicBoolean(false);
/** The preferences. */
private final Preferences preferences;
/** The event bus. */
private final EventBus eventBus;
/** The configuration. */
private final Configuration configuration;
/** Whether we’re currently saving the configuration. */
private boolean storingConfiguration = false;
/** The identity manager. */
private final IdentityManager identityManager;
/** Interface to freenet. */
private final FreenetInterface freenetInterface;
/** The Sone downloader. */
private final SoneDownloader soneDownloader;
/** The image inserter. */
private final ImageInserter imageInserter;
/** Sone downloader thread-pool. */
private final ExecutorService soneDownloaders = Executors.newFixedThreadPool(10, new NamedThreadFactory("Sone Downloader %2$d"));
/** The update checker. */
private final UpdateChecker updateChecker;
/** The trust updater. */
private final WebOfTrustUpdater webOfTrustUpdater;
/** Locked local Sones. */
/* synchronize on itself. */
private final Set lockedSones = new HashSet<>();
/** Sone inserters. */
/* synchronize access on this on sones. */
private final Map soneInserters = new HashMap<>();
/** Sone rescuers. */
/* synchronize access on this on sones. */
private final Map soneRescuers = new HashMap<>();
/** All known Sones. */
private final Set knownSones = new HashSet<>();
/** The post database. */
private final Database database;
/** Trusted identities, sorted by own identities. */
private final Multimap trustedIdentities = Multimaps.synchronizedSetMultimap(HashMultimap.create());
/** All temporary images. */
private final Map temporaryImages = new HashMap<>();
/** Ticker for threads that mark own elements as known. */
private final ScheduledExecutorService localElementTicker = Executors.newScheduledThreadPool(1);
/** The time the configuration was last touched. */
private volatile long lastConfigurationUpdate;
private final MetricRegistry metricRegistry;
private final Histogram configurationSaveTimeHistogram;
private final SoneUriCreator soneUriCreator;
@Inject
public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager, SoneDownloader soneDownloader, ImageInserter imageInserter, UpdateChecker updateChecker, WebOfTrustUpdater webOfTrustUpdater, EventBus eventBus, Database database, MetricRegistry metricRegistry, SoneUriCreator soneUriCreator) {
super("Sone Core");
this.configuration = configuration;
this.freenetInterface = freenetInterface;
this.identityManager = identityManager;
this.soneDownloader = soneDownloader;
this.imageInserter = imageInserter;
this.updateChecker = updateChecker;
this.webOfTrustUpdater = webOfTrustUpdater;
this.eventBus = eventBus;
this.database = database;
this.metricRegistry = metricRegistry;
this.soneUriCreator = soneUriCreator;
preferences = new Preferences(eventBus);
this.configurationSaveTimeHistogram = metricRegistry.histogram("configuration.save.duration", () -> new Histogram(new ExponentiallyDecayingReservoir(3000, 0)));
}
//
// ACCESSORS
//
/**
* Returns the time Sone was started.
*
* @return The startup time (in milliseconds since Jan 1, 1970 UTC)
*/
public long getStartupTime() {
return startupTime;
}
@Nonnull
public boolean getDebug() {
return debug.get();
}
public void setDebug() {
debug.set(true);
eventBus.post(new DebugActivatedEvent());
}
/**
* Returns the options used by the core.
*
* @return The options of the core
*/
public Preferences getPreferences() {
return preferences;
}
/**
* Returns the identity manager used by the core.
*
* @return The identity manager
*/
public IdentityManager getIdentityManager() {
return identityManager;
}
/**
* Returns the update checker.
*
* @return The update checker
*/
public UpdateChecker getUpdateChecker() {
return updateChecker;
}
/**
* Returns the Sone rescuer for the given local Sone.
*
* @param sone
* The local Sone to get the rescuer for
* @return The Sone rescuer for the given Sone
*/
public SoneRescuer getSoneRescuer(Sone sone) {
checkNotNull(sone, "sone must not be null");
checkArgument(sone.isLocal(), "sone must be local");
synchronized (soneRescuers) {
SoneRescuer soneRescuer = soneRescuers.get(sone);
if (soneRescuer == null) {
soneRescuer = new SoneRescuer(this, soneDownloader, sone);
soneRescuers.put(sone, soneRescuer);
soneRescuer.start();
}
return soneRescuer;
}
}
/**
* Returns whether the given Sone is currently locked.
*
* @param sone
* The sone to check
* @return {@code true} if the Sone is locked, {@code false} if it is not
*/
public boolean isLocked(Sone sone) {
synchronized (lockedSones) {
return lockedSones.contains(sone);
}
}
public SoneBuilder soneBuilder() {
return database.newSoneBuilder();
}
/**
* {@inheritDocs}
*/
@Nonnull
@Override
public Collection getSones() {
return database.getSones();
}
@Nonnull
@Override
public Function1 getSoneLoader() {
return database.getSoneLoader();
}
/**
* Returns the Sone with the given ID, regardless whether it’s local or
* remote.
*
* @param id
* The ID of the Sone to get
* @return The Sone with the given ID, or {@code null} if there is no such
* Sone
*/
@Override
@Nullable
public Sone getSone(@Nonnull String id) {
return database.getSone(id);
}
/**
* {@inheritDocs}
*/
@Override
public Collection getLocalSones() {
return database.getLocalSones();
}
/**
* Returns the local Sone with the given ID, optionally creating a new Sone.
*
* @param id
* The ID of the Sone
* @return The Sone with the given ID, or {@code null}
*/
public Sone getLocalSone(String id) {
Sone sone = database.getSone(id);
if ((sone != null) && sone.isLocal()) {
return sone;
}
return null;
}
/**
* {@inheritDocs}
*/
@Override
public Collection getRemoteSones() {
return database.getRemoteSones();
}
/**
* Returns the remote Sone with the given ID.
*
*
* @param id
* The ID of the remote Sone to get
* @return The Sone with the given ID
*/
public Sone getRemoteSone(String id) {
return database.getSone(id);
}
/**
* Returns whether the given Sone has been modified.
*
* @param sone
* The Sone to check for modifications
* @return {@code true} if a modification has been detected in the Sone,
* {@code false} otherwise
*/
public boolean isModifiedSone(Sone sone) {
return soneInserters.containsKey(sone) && soneInserters.get(sone).isModified();
}
/**
* Returns a post builder.
*
* @return A new post builder
*/
public PostBuilder postBuilder() {
return database.newPostBuilder();
}
@Nullable
@Override
public Post getPost(@Nonnull String postId) {
return database.getPost(postId);
}
/**
* {@inheritDocs}
*/
@Override
public Collection getPosts(String soneId) {
return database.getPosts(soneId);
}
/**
* {@inheritDoc}
*/
@Override
public Collection getDirectedPosts(final String recipientId) {
checkNotNull(recipientId, "recipient must not be null");
return database.getDirectedPosts(recipientId);
}
/**
* Returns a post reply builder.
*
* @return A new post reply builder
*/
public PostReplyBuilder postReplyBuilder() {
return database.newPostReplyBuilder();
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
public PostReply getPostReply(String replyId) {
return database.getPostReply(replyId);
}
/**
* {@inheritDoc}
*/
@Override
public List getReplies(final String postId) {
return database.getReplies(postId);
}
/**
* Returns all Sones that have liked the given post.
*
* @param post
* The post to get the liking Sones for
* @return The Sones that like the given post
*/
public Set getLikes(Post post) {
Set sones = new HashSet<>();
for (Sone sone : getSones()) {
if (sone.getLikedPostIds().contains(post.getId())) {
sones.add(sone);
}
}
return sones;
}
/**
* Returns all Sones that have liked the given reply.
*
* @param reply
* The reply to get the liking Sones for
* @return The Sones that like the given reply
*/
public Set getLikes(PostReply reply) {
Set sones = new HashSet<>();
for (Sone sone : getSones()) {
if (sone.getLikedReplyIds().contains(reply.getId())) {
sones.add(sone);
}
}
return sones;
}
/**
* Returns whether the given post is bookmarked.
*
* @param post
* The post to check
* @return {@code true} if the given post is bookmarked, {@code false}
* otherwise
*/
public boolean isBookmarked(Post post) {
return database.isPostBookmarked(post);
}
/**
* Returns all currently known bookmarked posts.
*
* @return All bookmarked posts
*/
public Set getBookmarkedPosts() {
return database.getBookmarkedPosts();
}
public AlbumBuilder albumBuilder() {
return database.newAlbumBuilder();
}
/**
* Returns the album with the given ID, optionally creating a new album if
* an album with the given ID can not be found.
*
* @param albumId
* The ID of the album
* @return The album with the given ID, or {@code null} if no album with the
* given ID exists
*/
@Nullable
public Album getAlbum(@Nonnull String albumId) {
return database.getAlbum(albumId);
}
public ImageBuilder imageBuilder() {
return database.newImageBuilder();
}
/**
* Returns the image with the given ID, creating it if necessary.
*
* @param imageId
* The ID of the image
* @return The image with the given ID
*/
@Nullable
public Image getImage(String imageId) {
return getImage(imageId, true);
}
/**
* Returns the image with the given ID, optionally creating it if it does
* not exist.
*
* @param imageId
* The ID of the image
* @param create
* {@code true} to create an image if none exists with the given
* ID
* @return The image with the given ID, or {@code null} if none exists and
* none was created
*/
@Nullable
public Image getImage(String imageId, boolean create) {
Image image = database.getImage(imageId);
if (image != null) {
return image;
}
if (!create) {
return null;
}
Image newImage = database.newImageBuilder().withId(imageId).build();
database.storeImage(newImage);
return newImage;
}
/**
* Returns the temporary image with the given ID.
*
* @param imageId
* The ID of the temporary image
* @return The temporary image, or {@code null} if there is no temporary
* image with the given ID
*/
public TemporaryImage getTemporaryImage(String imageId) {
synchronized (temporaryImages) {
return temporaryImages.get(imageId);
}
}
//
// ACTIONS
//
/**
* Locks the given Sone. A locked Sone will not be inserted by
* {@link SoneInserter} until it is {@link #unlockSone(Sone) unlocked}
* again.
*
* @param sone
* The sone to lock
*/
public void lockSone(Sone sone) {
synchronized (lockedSones) {
if (lockedSones.add(sone)) {
eventBus.post(new SoneLockedEvent(sone));
}
}
}
/**
* Unlocks the given Sone.
*
* @see #lockSone(Sone)
* @param sone
* The sone to unlock
*/
public void unlockSone(Sone sone) {
synchronized (lockedSones) {
if (lockedSones.remove(sone)) {
eventBus.post(new SoneUnlockedEvent(sone));
}
}
}
/**
* Adds a local Sone from the given own identity.
*
* @param ownIdentity
* The own identity to create a Sone from
* @return The added (or already existing) Sone
*/
public Sone addLocalSone(OwnIdentity ownIdentity) {
if (ownIdentity == null) {
logger.log(Level.WARNING, "Given OwnIdentity is null!");
return null;
}
logger.info(String.format("Adding Sone from OwnIdentity: %s", ownIdentity));
Sone sone = database.newSoneBuilder().local().from(ownIdentity).build();
String property = fromNullable(ownIdentity.getProperty("Sone.LatestEdition")).or("0");
sone.setLatestEdition(fromNullable(tryParse(property)).or(0L));
sone.setClient(new Client("Sone", SonePlugin.getPluginVersion()));
sone.setKnown(true);
SoneInserter soneInserter = new SoneInserter(this, eventBus, freenetInterface, metricRegistry, soneUriCreator, ownIdentity.getId());
soneInserter.insertionDelayChanged(new InsertionDelayChangedEvent(preferences.getInsertionDelay()));
eventBus.register(soneInserter);
synchronized (soneInserters) {
soneInserters.put(sone, soneInserter);
}
loadSone(sone);
database.storeSone(sone);
sone.setStatus(SoneStatus.idle);
if (sone.getPosts().isEmpty() && sone.getReplies().isEmpty() && getAllImages(sone.getRootAlbum()).isEmpty()) {
// dirty hack
lockSone(sone);
eventBus.post(new SoneLockedOnStartup(sone));
}
soneInserter.start();
return sone;
}
/**
* Creates a new Sone for the given own identity.
*
* @param ownIdentity
* The own identity to create a Sone for
* @return The created Sone
*/
public Sone createSone(OwnIdentity ownIdentity) {
if (!webOfTrustUpdater.addContextWait(ownIdentity, "Sone")) {
logger.log(Level.SEVERE, String.format("Could not add “Sone” context to own identity: %s", ownIdentity));
return null;
}
Sone sone = addLocalSone(ownIdentity);
followSone(sone, "nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI");
touchConfiguration();
return sone;
}
/**
* Adds the Sone of the given identity.
*
* @param identity
* The identity whose Sone to add
* @return The added or already existing Sone
*/
public Sone addRemoteSone(Identity identity) {
if (identity == null) {
logger.log(Level.WARNING, "Given Identity is null!");
return null;
}
String property = fromNullable(identity.getProperty("Sone.LatestEdition")).or("0");
long latestEdition = fromNullable(tryParse(property)).or(0L);
Sone existingSone = getSone(identity.getId());
if ((existingSone != null )&& existingSone.isLocal()) {
return existingSone;
}
boolean newSone = existingSone == null;
Sone sone = !newSone ? existingSone : database.newSoneBuilder().from(identity).build();
sone.setLatestEdition(latestEdition);
if (newSone) {
synchronized (knownSones) {
newSone = !knownSones.contains(sone.getId());
}
sone.setKnown(!newSone);
if (newSone) {
eventBus.post(new NewSoneFoundEvent(sone));
for (Sone localSone : getLocalSones()) {
if (localSone.getOptions().isAutoFollow()) {
followSone(localSone, sone.getId());
}
}
}
}
database.storeSone(sone);
soneDownloader.addSone(sone);
soneDownloaders.execute(soneDownloader.fetchSoneAsUskAction(sone));
return sone;
}
/**
* Lets the given local Sone follow the Sone with the given ID.
*
* @param sone
* The local Sone that should follow another Sone
* @param soneId
* The ID of the Sone to follow
*/
public void followSone(Sone sone, String soneId) {
checkNotNull(sone, "sone must not be null");
checkNotNull(soneId, "soneId must not be null");
database.addFriend(sone, soneId);
@SuppressWarnings("ConstantConditions") // we just followed, this can’t be null.
long now = database.getFollowingTime(soneId);
Sone followedSone = getSone(soneId);
if (followedSone == null) {
return;
}
for (Post post : followedSone.getPosts()) {
if (post.getTime() < now) {
markPostKnown(post);
}
}
for (PostReply reply : followedSone.getReplies()) {
if (reply.getTime() < now) {
markReplyKnown(reply);
}
}
touchConfiguration();
}
/**
* Lets the given local Sone unfollow the Sone with the given ID.
*
* @param sone
* The local Sone that should unfollow another Sone
* @param soneId
* The ID of the Sone being unfollowed
*/
public void unfollowSone(Sone sone, String soneId) {
checkNotNull(sone, "sone must not be null");
checkNotNull(soneId, "soneId must not be null");
database.removeFriend(sone, soneId);
touchConfiguration();
}
/**
* Updates the stored Sone with the given Sone.
*
* @param sone
* The updated Sone
*/
public void updateSone(Sone sone) {
updateSone(sone, false);
}
/**
* Updates the stored Sone with the given Sone. If {@code soneRescueMode} is
* {@code true}, an older Sone than the current Sone can be given to restore
* an old state.
*
* @param sone
* The Sone to update
* @param soneRescueMode
* {@code true} if the stored Sone should be updated regardless
* of the age of the given Sone
*/
public void updateSone(final Sone sone, boolean soneRescueMode) {
Sone storedSone = getSone(sone.getId());
if (storedSone != null) {
if (!soneRescueMode && !(sone.getTime() > storedSone.getTime())) {
logger.log(Level.FINE, String.format("Downloaded Sone %s is not newer than stored Sone %s.", sone, storedSone));
return;
}
List