X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=blobdiff_plain;f=src%2Fmain%2Fjava%2Fnet%2Fpterodactylus%2Fsone%2Fcore%2FCore.java;h=6cd61355c625d57736003caaf6afde0512d54ac7;hp=48976b172d7f83d47b1575e2b56d085692c27e96;hb=d0104f792aec1befd7c18bb46c174682dc416322;hpb=4716b1f56bd899365c7a55e02f3356f5af071ca5 diff --git a/src/main/java/net/pterodactylus/sone/core/Core.java b/src/main/java/net/pterodactylus/sone/core/Core.java index 48976b1..6cd6135 100644 --- a/src/main/java/net/pterodactylus/sone/core/Core.java +++ b/src/main/java/net/pterodactylus/sone/core/Core.java @@ -1,5 +1,5 @@ /* - * FreenetSone - Core.java - Copyright © 2010 David Roden + * Sone - Core.java - Copyright © 2010 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 @@ -18,24 +18,31 @@ package net.pterodactylus.sone.core; import java.net.MalformedURLException; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; -import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; -import net.pterodactylus.sone.core.SoneException.Type; +import net.pterodactylus.sone.core.Options.DefaultOption; +import net.pterodactylus.sone.core.Options.Option; +import net.pterodactylus.sone.core.Options.OptionWatcher; import net.pterodactylus.sone.data.Post; import net.pterodactylus.sone.data.Profile; import net.pterodactylus.sone.data.Reply; import net.pterodactylus.sone.data.Sone; +import net.pterodactylus.sone.freenet.wot.Identity; +import net.pterodactylus.sone.freenet.wot.IdentityListener; +import net.pterodactylus.sone.freenet.wot.IdentityManager; +import net.pterodactylus.sone.freenet.wot.OwnIdentity; import net.pterodactylus.util.config.Configuration; import net.pterodactylus.util.config.ConfigurationException; import net.pterodactylus.util.logging.Logging; -import net.pterodactylus.util.service.AbstractService; +import net.pterodactylus.util.number.Numbers; import freenet.keys.FreenetURI; /** @@ -43,42 +50,83 @@ import freenet.keys.FreenetURI; * * @author David ‘Bombe’ Roden */ -public class Core extends AbstractService { +public class Core implements IdentityListener { + + /** + * Enumeration for the possible states of a {@link Sone}. + * + * @author David ‘Bombe’ Roden + */ + public enum SoneStatus { + + /** The Sone is unknown, i.e. not yet downloaded. */ + unknown, + + /** The Sone is idle, i.e. not being downloaded or inserted. */ + idle, + + /** The Sone is currently being inserted. */ + inserting, + + /** The Sone is currently being downloaded. */ + downloading, + } /** The logger. */ private static final Logger logger = Logging.getLogger(Core.class); + /** The options. */ + private final Options options = new Options(); + /** The configuration. */ - private Configuration configuration; + private final Configuration configuration; + + /** The identity manager. */ + private final IdentityManager identityManager; /** Interface to freenet. */ - private FreenetInterface freenetInterface; + private final FreenetInterface freenetInterface; /** The Sone downloader. */ - private SoneDownloader soneDownloader; + private final SoneDownloader soneDownloader; - /** The local Sones. */ - private final Set localSones = new HashSet(); + /** The Sones’ statuses. */ + /* synchronize access on itself. */ + private final Map soneStatuses = new HashMap(); /** Sone inserters. */ + /* synchronize access on this on localSones. */ private final Map soneInserters = new HashMap(); - /* various caches follow here. */ + /** All local Sones. */ + /* synchronize access on this on itself. */ + private Map localSones = new HashMap(); - /** Cache for all known Sones. */ - private final Map soneCache = new HashMap(); + /** All remote Sones. */ + /* synchronize access on this on itself. */ + private Map remoteSones = new HashMap(); - /** Cache for all known posts. */ - private final Map postCache = new HashMap(); + /** All posts. */ + private Map posts = new HashMap(); - /** Cache for all known replies. */ - private final Map replyCache = new HashMap(); + /** All replies. */ + private Map replies = new HashMap(); /** * Creates a new core. + * + * @param configuration + * The configuration of the core + * @param freenetInterface + * The freenet interface + * @param identityManager + * The identity manager */ - public Core() { - super("Sone Core"); + public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager) { + this.configuration = configuration; + this.freenetInterface = freenetInterface; + this.identityManager = identityManager; + this.soneDownloader = new SoneDownloader(this, freenetInterface); } // @@ -86,54 +134,266 @@ public class Core extends AbstractService { // /** - * Sets the configuration of the core. + * Returns the options used by the core. * - * @param configuration - * The configuration of the core - * @return This core (for method chaining) + * @return The options of the core */ - public Core configuration(Configuration configuration) { - this.configuration = configuration; - return this; + public Options getOptions() { + return options; } /** - * Sets the Freenet interface to use. + * Returns the identity manager used by the core. * - * @param freenetInterface - * The Freenet interface to use - * @return This core (for method chaining) + * @return The identity manager */ - public Core freenetInterface(FreenetInterface freenetInterface) { - this.freenetInterface = freenetInterface; - soneDownloader = new SoneDownloader(freenetInterface); - soneDownloader.start(); - return this; + public IdentityManager getIdentityManager() { + return identityManager; + } + + /** + * Returns the status of the given Sone. + * + * @param sone + * The Sone to get the status for + * @return The status of the Sone + */ + public SoneStatus getSoneStatus(Sone sone) { + synchronized (soneStatuses) { + return soneStatuses.get(sone); + } + } + + /** + * Sets the status of the given Sone. + * + * @param sone + * The Sone to set the status of + * @param soneStatus + * The status to set + */ + public void setSoneStatus(Sone sone, SoneStatus soneStatus) { + synchronized (soneStatuses) { + soneStatuses.put(sone, soneStatus); + } } /** - * Returns the local Sones. + * Returns all Sones, remote and local. * - * @return The local Sones + * @return All Sones */ public Set getSones() { - return Collections.unmodifiableSet(localSones); + Set allSones = new HashSet(); + allSones.addAll(getLocalSones()); + allSones.addAll(getRemoteSones()); + return allSones; + } + + /** + * 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 + */ + public Sone getSone(String id) { + if (isLocalSone(id)) { + return getLocalSone(id); + } + return getRemoteSone(id); + } + + /** + * Returns whether the given Sone is a local Sone. + * + * @param sone + * The Sone to check for its locality + * @return {@code true} if the given Sone is local, {@code false} otherwise + */ + public boolean isLocalSone(Sone sone) { + synchronized (localSones) { + return localSones.containsKey(sone.getId()); + } + } + + /** + * Returns whether the given ID is the ID of a local Sone. + * + * @param id + * The Sone ID to check for its locality + * @return {@code true} if the given ID is a local Sone, {@code false} + * otherwise + */ + public boolean isLocalSone(String id) { + synchronized (localSones) { + return localSones.containsKey(id); + } + } + + /** + * Returns all local Sones. + * + * @return All local Sones + */ + public Set getLocalSones() { + synchronized (localSones) { + return new HashSet(localSones.values()); + } + } + + /** + * Returns the local Sone with the given ID. + * + * @param id + * The ID of the Sone to get + * @return The Sone with the given ID + */ + public Sone getLocalSone(String id) { + synchronized (localSones) { + Sone sone = localSones.get(id); + if (sone == null) { + sone = new Sone(id); + localSones.put(id, sone); + } + return sone; + } + } + + /** + * Returns all remote Sones. + * + * @return All remote Sones + */ + public Set getRemoteSones() { + synchronized (remoteSones) { + return new HashSet(remoteSones.values()); + } + } + + /** + * 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) { + synchronized (remoteSones) { + Sone sone = remoteSones.get(id); + if (sone == null) { + sone = new Sone(id); + remoteSones.put(id, sone); + } + return sone; + } + } + + /** + * Returns whether the given Sone is a remote Sone. + * + * @param sone + * The Sone to check + * @return {@code true} if the given Sone is a remote Sone, {@code false} + * otherwise + */ + public boolean isRemoteSone(Sone sone) { + synchronized (remoteSones) { + return remoteSones.containsKey(sone.getId()); + } + } + + /** + * Returns the post with the given ID. + * + * @param postId + * The ID of the post to get + * @return The post, or {@code null} if there is no such post + */ + public Post getPost(String postId) { + synchronized (posts) { + Post post = posts.get(postId); + if (post == null) { + post = new Post(postId); + posts.put(postId, post); + } + return post; + } + } + + /** + * Returns the reply with the given ID. + * + * @param replyId + * The ID of the reply to get + * @return The reply, or {@code null} if there is no such reply + */ + public Reply getReply(String replyId) { + synchronized (replies) { + Reply reply = replies.get(replyId); + if (reply == null) { + reply = new Reply(replyId); + replies.put(replyId, reply); + } + return reply; + } } /** - * Returns the Sone with the given ID, or an empty Sone that has been - * initialized with the given ID. + * Returns all replies for the given post, order ascending by time. * - * @param soneId - * The ID of the Sone - * @return The Sone + * @param post + * The post to get all replies for + * @return All replies for the given post */ - public Sone getSone(String soneId) { - if (!soneCache.containsKey(soneId)) { - Sone sone = new Sone(soneId); - soneCache.put(soneId, sone); + public List getReplies(Post post) { + Set sones = getSones(); + List replies = new ArrayList(); + for (Sone sone : sones) { + for (Reply reply : sone.getReplies()) { + if (reply.getPost().equals(post)) { + replies.add(reply); + } + } } - return soneCache.get(soneId); + Collections.sort(replies, Reply.TIME_COMPARATOR); + return replies; + } + + /** + * 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(Reply reply) { + Set sones = new HashSet(); + for (Sone sone : getSones()) { + if (sone.getLikedPostIds().contains(reply.getId())) { + sones.add(sone); + } + } + return sones; } // @@ -141,125 +401,381 @@ public class Core extends AbstractService { // /** - * Adds the given Sone. + * Adds a local Sone from the given ID which has to be the ID of an own + * identity. * - * @param sone - * The Sone to add + * @param id + * The ID of an own identity to add a Sone for + * @return The added (or already existing) Sone */ - public void addLocalSone(Sone sone) { - if (localSones.add(sone)) { - soneCache.put(sone.getId(), sone); - SoneInserter soneInserter = new SoneInserter(freenetInterface, sone); + public Sone addLocalSone(String id) { + synchronized (localSones) { + if (localSones.containsKey(id)) { + logger.log(Level.FINE, "Tried to add known local Sone: %s", id); + return localSones.get(id); + } + OwnIdentity ownIdentity = identityManager.getOwnIdentity(id); + if (ownIdentity == null) { + logger.log(Level.INFO, "Invalid Sone ID: %s", id); + return null; + } + return addLocalSone(ownIdentity); + } + } + + /** + * 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; + } + synchronized (localSones) { + if (localSones.containsKey(ownIdentity.getId())) { + logger.log(Level.FINE, "Tried to add known local Sone: %s", ownIdentity); + return localSones.get(ownIdentity.getId()); + } + final Sone sone; + try { + sone = getLocalSone(ownIdentity.getId()).setIdentity(ownIdentity).setInsertUri(new FreenetURI(ownIdentity.getInsertUri())).setRequestUri(new FreenetURI(ownIdentity.getRequestUri())); + sone.setLatestEdition(Numbers.safeParseLong(ownIdentity.getProperty("Sone.LatestEdition"), (long) 0)); + } catch (MalformedURLException mue1) { + logger.log(Level.SEVERE, "Could not convert the Identity’s URIs to Freenet URIs: " + ownIdentity.getInsertUri() + ", " + ownIdentity.getRequestUri(), mue1); + return null; + } + /* TODO - load posts ’n stuff */ + localSones.put(ownIdentity.getId(), sone); + SoneInserter soneInserter = new SoneInserter(this, freenetInterface, sone); + soneInserters.put(sone, soneInserter); soneInserter.start(); + setSoneStatus(sone, SoneStatus.idle); + new Thread(new Runnable() { + + @Override + @SuppressWarnings("synthetic-access") + public void run() { + soneDownloader.fetchSone(sone); + } + + }, "Sone Downloader").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) { + identityManager.addContext(ownIdentity, "Sone"); + Sone sone = addLocalSone(ownIdentity); + synchronized (sone) { + /* mark as modified so that it gets inserted immediately. */ + sone.setModificationCounter(sone.getModificationCounter() + 1); + } + 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; + } + synchronized (remoteSones) { + if (remoteSones.containsKey(identity.getId())) { + logger.log(Level.FINE, "Identity already exists: %s", identity); + return remoteSones.get(identity.getId()); + } + Sone sone = new Sone(identity); + sone.setRequestUri(getSoneUri(identity.getRequestUri(), identity.getProperty("Sone.LatestEdition"))); + remoteSones.put(identity.getId(), sone); soneDownloader.addSone(sone); - soneInserters.put(sone, soneInserter); + setSoneStatus(sone, SoneStatus.idle); + return sone; } } /** - * Adds a remote Sone so that it is watched for updates. + * Updates the stores Sone with the given Sone. * * @param sone - * The sone to watch + * The updated Sone */ - public void addSone(Sone sone) { - if (!soneCache.containsKey(sone.getId())) { - soneCache.put(sone.getId(), sone); + public void updateSone(Sone sone) { + if (isRemoteSone(sone)) { + Sone storedSone = getRemoteSone(sone.getId()); + if (!(sone.getTime() > storedSone.getTime())) { + logger.log(Level.FINE, "Downloaded Sone %s is not newer than stored Sone %s.", new Object[] { sone, storedSone }); + return; + } + synchronized (posts) { + for (Post post : storedSone.getPosts()) { + posts.remove(post.getId()); + } + for (Post post : sone.getPosts()) { + posts.put(post.getId(), post); + } + } + synchronized (replies) { + for (Reply reply : storedSone.getReplies()) { + replies.remove(reply.getId()); + } + for (Reply reply : sone.getReplies()) { + replies.put(reply.getId(), reply); + } + } + synchronized (storedSone) { + storedSone.setTime(sone.getTime()); + storedSone.setProfile(sone.getProfile()); + storedSone.setPosts(sone.getPosts()); + storedSone.setReplies(sone.getReplies()); + storedSone.setLikePostIds(sone.getLikedPostIds()); + storedSone.setLikeReplyIds(sone.getLikedReplyIds()); + storedSone.setLatestEdition(sone.getRequestUri().getEdition()); + } + saveSone(storedSone); } - soneDownloader.addSone(sone); } /** - * Creates a new Sone at a random location. + * Deletes the given Sone. This will remove the Sone from the + * {@link #getLocalSone(String) local Sones}, stops its {@link SoneInserter} + * and remove the context from its identity. * - * @param name - * The name of the Sone - * @return The created Sone - * @throws SoneException - * if a Sone error occurs + * @param sone + * The Sone to delete */ - public Sone createSone(String name) throws SoneException { - return createSone(name, null, null); + public void deleteSone(Sone sone) { + if (!(sone.getIdentity() instanceof OwnIdentity)) { + logger.log(Level.WARNING, "Tried to delete Sone of non-own identity: %s", sone); + return; + } + synchronized (localSones) { + if (!localSones.containsKey(sone.getId())) { + logger.log(Level.WARNING, "Tried to delete non-local Sone: %s", sone); + return; + } + localSones.remove(sone.getId()); + soneInserters.remove(sone.getId()).stop(); + } + identityManager.removeContext((OwnIdentity) sone.getIdentity(), "Sone"); } /** - * Creates a new Sone at the given location. If one of {@code requestUri} or - * {@code insertUrI} is {@code null}, the Sone is created at a random - * location. + * Saves the given Sone. This will persist all local settings for the given + * Sone, such as the friends list and similar, private options. * - * @param name - * The name of the Sone - * @param requestUri - * The request URI of the Sone, or {@link NullPointerException} - * to create a Sone at a random location - * @param insertUri - * The insert URI of the Sone, or {@code null} to create a Sone - * at a random location - * @return The created Sone - * @throws SoneException - * if a Sone error occurs - */ - public Sone createSone(String name, String requestUri, String insertUri) throws SoneException { - if ((name == null) || (name.trim().length() == 0)) { - throw new SoneException(Type.INVALID_SONE_NAME); - } - String finalRequestUri; - String finalInsertUri; - if ((requestUri == null) || (insertUri == null)) { - String[] keyPair = freenetInterface.generateKeyPair(); - finalRequestUri = keyPair[0]; - finalInsertUri = keyPair[1]; - } else { - finalRequestUri = requestUri; - finalInsertUri = insertUri; - } - Sone sone; + * @param sone + * The Sone to save + */ + public void saveSone(Sone sone) { + if (!isLocalSone(sone)) { + logger.log(Level.FINE, "Tried to save non-local Sone: %s", sone); + return; + } + if (!(sone.getIdentity() instanceof OwnIdentity)) { + logger.log(Level.WARNING, "Local Sone without OwnIdentity found, refusing to save: %s", sone); + return; + } + + logger.log(Level.INFO, "Saving Sone: %s", sone); + identityManager.setProperty((OwnIdentity) sone.getIdentity(), "Sone.LatestEdition", String.valueOf(sone.getLatestEdition())); try { - logger.log(Level.FINEST, "Creating new Sone “%s” at %s (%s)…", new Object[] { name, finalRequestUri, finalInsertUri }); - sone = new Sone(UUID.randomUUID().toString()).setName(name).setRequestUri(new FreenetURI(finalRequestUri).setKeyType("USK").setDocName("Sone-" + name)).setInsertUri(new FreenetURI(finalInsertUri).setKeyType("USK").setDocName("Sone-" + name)); - sone.setProfile(new Profile()); - /* set modification counter to 1 so it is inserted immediately. */ - sone.setModificationCounter(1); - addLocalSone(sone); - } catch (MalformedURLException mue1) { - throw new SoneException(Type.INVALID_URI); + /* save Sone into configuration. */ + String sonePrefix = "Sone/" + sone.getId(); + configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime()); + + /* save profile. */ + Profile profile = sone.getProfile(); + configuration.getStringValue(sonePrefix + "/Profile/FirstName").setValue(profile.getFirstName()); + configuration.getStringValue(sonePrefix + "/Profile/MiddleName").setValue(profile.getMiddleName()); + configuration.getStringValue(sonePrefix + "/Profile/LastName").setValue(profile.getLastName()); + configuration.getIntValue(sonePrefix + "/Profile/BirthDay").setValue(profile.getBirthDay()); + configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth()); + configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear()); + + /* save posts. */ + int postCounter = 0; + for (Post post : sone.getPosts()) { + String postPrefix = sonePrefix + "/Posts/" + postCounter++; + configuration.getStringValue(postPrefix + "/ID").setValue(post.getId()); + configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime()); + configuration.getStringValue(postPrefix + "/Text").setValue(post.getText()); + } + configuration.getStringValue(sonePrefix + "/Posts/" + postCounter + "/ID").setValue(null); + + /* save replies. */ + int replyCounter = 0; + for (Reply reply : sone.getReplies()) { + String replyPrefix = sonePrefix + "/Replies/" + replyCounter++; + configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId()); + configuration.getStringValue(replyPrefix + "/Post/ID").setValue(reply.getPost().getId()); + configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime()); + configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText()); + } + configuration.getStringValue(sonePrefix + "/Replies/" + replyCounter + "/ID").setValue(null); + + /* save post likes. */ + int postLikeCounter = 0; + for (String postId : sone.getLikedPostIds()) { + configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter++ + "/ID").setValue(postId); + } + configuration.getStringValue(sonePrefix + "/Likes/Post/" + postLikeCounter + "/ID").setValue(null); + + /* save reply likes. */ + int replyLikeCounter = 0; + for (String replyId : sone.getLikedReplyIds()) { + configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter++ + "/ID").setValue(replyId); + } + configuration.getStringValue(sonePrefix + "/Likes/Reply/" + replyLikeCounter + "/ID").setValue(null); + + logger.log(Level.INFO, "Sone %s saved.", sone); + } catch (ConfigurationException ce1) { + logger.log(Level.WARNING, "Could not save Sone: " + sone, ce1); } - return sone; } /** - * Deletes the given Sone from this plugin instance. + * Creates a new post. * * @param sone - * The sone to delete + * The Sone that creates the post + * @param text + * The text of the post */ - public void deleteSone(Sone sone) { - SoneInserter soneInserter = soneInserters.remove(sone); - soneInserter.stop(); - localSones.remove(sone); + public void createPost(Sone sone, String text) { + createPost(sone, System.currentTimeMillis(), text); } - // - // SERVICE METHODS - // + /** + * Creates a new post. + * + * @param sone + * The Sone that creates the post + * @param time + * The time of the post + * @param text + * The text of the post + */ + public void createPost(Sone sone, long time, String text) { + if (!isLocalSone(sone)) { + logger.log(Level.FINE, "Tried to create post for non-local Sone: %s", sone); + return; + } + Post post = new Post(sone, time, text); + synchronized (posts) { + posts.put(post.getId(), post); + } + sone.addPost(post); + saveSone(sone); + } /** - * {@inheritDoc} + * Deletes the given post. + * + * @param post + * The post to delete */ - @Override - protected void serviceStart() { + public void deletePost(Post post) { + if (!isLocalSone(post.getSone())) { + logger.log(Level.WARNING, "Tried to delete post of non-local Sone: %s", post.getSone()); + return; + } + post.getSone().removePost(post); + synchronized (posts) { + posts.remove(post.getId()); + } + saveSone(post.getSone()); + } + + /** + * Creates a new reply. + * + * @param sone + * The Sone that creates the reply + * @param post + * The post that this reply refers to + * @param text + * The text of the reply + */ + public void createReply(Sone sone, Post post, String text) { + createReply(sone, post, System.currentTimeMillis(), text); + } + + /** + * Creates a new reply. + * + * @param sone + * The Sone that creates the reply + * @param post + * The post that this reply refers to + * @param time + * The time of the reply + * @param text + * The text of the reply + */ + public void createReply(Sone sone, Post post, long time, String text) { + if (!isLocalSone(sone)) { + logger.log(Level.FINE, "Tried to create reply for non-local Sone: %s", sone); + return; + } + Reply reply = new Reply(sone, post, System.currentTimeMillis(), text); + synchronized (replies) { + replies.put(reply.getId(), reply); + } + sone.addReply(reply); + saveSone(sone); + } + + /** + * Deletes the given reply. + * + * @param reply + * The reply to delete + */ + public void deleteReply(Reply reply) { + Sone sone = reply.getSone(); + if (!isLocalSone(sone)) { + logger.log(Level.FINE, "Tried to delete non-local reply: %s", reply); + return; + } + synchronized (replies) { + replies.remove(reply.getId()); + } + sone.removeReply(reply); + saveSone(sone); + } + + /** + * Starts the core. + */ + public void start() { loadConfiguration(); } /** - * {@inheritDoc} + * Stops the core. */ - @Override - protected void serviceStop() { - soneDownloader.stop(); - /* stop all Sone inserters. */ - for (SoneInserter soneInserter : soneInserters.values()) { - soneInserter.stop(); + public void stop() { + synchronized (localSones) { + for (SoneInserter soneInserter : soneInserters.values()) { + soneInserter.stop(); + } } saveConfiguration(); } @@ -271,152 +787,114 @@ public class Core extends AbstractService { /** * Loads the configuration. */ + @SuppressWarnings("unchecked") private void loadConfiguration() { - logger.entering(Core.class.getName(), "loadConfiguration()"); - - /* parse local Sones. */ - logger.log(Level.INFO, "Loading Sones…"); - int soneId = 0; - do { - String sonePrefix = "Sone/Sone." + soneId++; - String id = configuration.getStringValue(sonePrefix + "/ID").getValue(null); - if (id == null) { - break; - } - String name = configuration.getStringValue(sonePrefix + "/Name").getValue(null); - String insertUri = configuration.getStringValue(sonePrefix + "/InsertURI").getValue(null); - String requestUri = configuration.getStringValue(sonePrefix + "/RequestURI").getValue(null); - long modificationCounter = configuration.getLongValue(sonePrefix + "/ModificationCounter").getValue((long) 0); - String firstName = configuration.getStringValue(sonePrefix + "/Profile/FirstName").getValue(null); - String middleName = configuration.getStringValue(sonePrefix + "/Profile/MiddleName").getValue(null); - String lastName = configuration.getStringValue(sonePrefix + "/Profile/LastName").getValue(null); - try { - Profile profile = new Profile(); - profile.setFirstName(firstName); - profile.setMiddleName(middleName); - profile.setLastName(lastName); - Sone sone = new Sone(id).setName(name).setRequestUri(new FreenetURI(requestUri)).setInsertUri(new FreenetURI(insertUri)); - soneCache.put(id, sone); - sone.setProfile(profile); - int postId = 0; - do { - String postPrefix = sonePrefix + "/Post." + postId++; - id = configuration.getStringValue(postPrefix + "/ID").getValue(null); - if (id == null) { - break; - } - long time = configuration.getLongValue(postPrefix + "/Time").getValue(null); - String text = configuration.getStringValue(postPrefix + "/Text").getValue(null); - Post post = new Post(id, sone, time, text); - postCache.put(id, post); - sone.addPost(post); - } while (true); - int replyCounter = 0; - do { - String replyPrefix = sonePrefix + "/Reply." + replyCounter++; - String replyId = configuration.getStringValue(replyPrefix + "/ID").getValue(null); - if (replyId == null) { - break; - } - Sone replySone = getSone(configuration.getStringValue(replyPrefix + "/Sone/ID").getValue(null)); - String replySoneKey = configuration.getStringValue(replyPrefix + "/Sone/Key").getValue(null); - String replySoneName = configuration.getStringValue(replyPrefix + "/Sone/Name").getValue(null); - replySone.setRequestUri(new FreenetURI(replySoneKey)).setName(replySoneName); - Post replyPost = postCache.get(configuration.getStringValue(replyPrefix + "/Post").getValue(null)); - long replyTime = configuration.getLongValue(replyPrefix + "/Time").getValue(null); - String replyText = configuration.getStringValue(replyPrefix + "/Text").getValue(null); - Reply reply = new Reply(replyId, replySone, replyPost, replyTime, replyText); - replyCache.put(replyId, reply); - } while (true); - - /* load friends. */ - int friendCounter = 0; - while (true) { - String friendPrefix = sonePrefix + "/Friend." + friendCounter++; - String friendId = configuration.getStringValue(friendPrefix + "/ID").getValue(null); - if (friendId == null) { - break; - } - Sone friendSone = getSone(friendId); - String friendKey = configuration.getStringValue(friendPrefix + "/Key").getValue(null); - String friendName = configuration.getStringValue(friendPrefix + "/Name").getValue(null); - friendSone.setRequestUri(new FreenetURI(friendKey)).setName(friendName); - addSone(friendSone); - sone.addFriend(sone); - } + /* create options. */ + options.addIntegerOption("InsertionDelay", new DefaultOption(60, new OptionWatcher() { - sone.setModificationCounter(modificationCounter); - addLocalSone(sone); - } catch (MalformedURLException mue1) { - logger.log(Level.WARNING, "Could not create Sone from requestUri (“" + requestUri + "”) and insertUri (“" + insertUri + "”)!", mue1); + @Override + public void optionChanged(Option option, Integer oldValue, Integer newValue) { + SoneInserter.setInsertionDelay(newValue); } - } while (true); - logger.log(Level.INFO, "Loaded %d Sones.", getSones().size()); - logger.exiting(Core.class.getName(), "loadConfiguration()"); + })); + options.addBooleanOption("ClearOnNextRestart", new DefaultOption(false)); + options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption(false)); + + /* read options from configuration. */ + options.getBooleanOption("ClearOnNextRestart").set(configuration.getBooleanValue("Option/ClearOnNextRestart").getValue(null)); + options.getBooleanOption("ReallyClearOnNextRestart").set(configuration.getBooleanValue("Option/ReallyClearOnNextRestart").getValue(null)); + boolean clearConfiguration = options.getBooleanOption("ClearOnNextRestart").get() && options.getBooleanOption("ReallyClearOnNextRestart").get(); + options.getBooleanOption("ClearOnNextRestart").set(null); + options.getBooleanOption("ReallyClearOnNextRestart").set(null); + if (clearConfiguration) { + /* stop loading the configuration. */ + return; + } + + options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null)); + } /** - * Saves the configuraiton. + * Saves the current options. */ private void saveConfiguration() { - Set sones = getSones(); - logger.log(Level.INFO, "Storing %d Sones…", sones.size()); + /* store the options first. */ try { - /* store all Sones. */ - int soneId = 0; - for (Sone sone : localSones) { - String sonePrefix = "Sone/Sone." + soneId++; - configuration.getStringValue(sonePrefix + "/ID").setValue(sone.getId()); - configuration.getStringValue(sonePrefix + "/Name").setValue(sone.getName()); - configuration.getStringValue(sonePrefix + "/RequestURI").setValue(sone.getRequestUri().toString()); - configuration.getStringValue(sonePrefix + "/InsertURI").setValue(sone.getInsertUri().toString()); - configuration.getLongValue(sonePrefix + "/ModificationCounter").setValue(sone.getModificationCounter()); - Profile profile = sone.getProfile(); - configuration.getStringValue(sonePrefix + "/Profile/FirstName").setValue(profile.getFirstName()); - configuration.getStringValue(sonePrefix + "/Profile/MiddleName").setValue(profile.getMiddleName()); - configuration.getStringValue(sonePrefix + "/Profile/LastName").setValue(profile.getLastName()); - int postId = 0; - for (Post post : sone.getPosts()) { - String postPrefix = sonePrefix + "/Post." + postId++; - configuration.getStringValue(postPrefix + "/ID").setValue(post.getId()); - configuration.getLongValue(postPrefix + "/Time").setValue(post.getTime()); - configuration.getStringValue(postPrefix + "/Text").setValue(post.getText()); - } - /* write null ID as terminator. */ - configuration.getStringValue(sonePrefix + "/Post." + postId + "/ID").setValue(null); + configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal()); + configuration.getBooleanValue("Option/ClearOnNextRestart").setValue(options.getBooleanOption("ClearOnNextRestart").getReal()); + configuration.getBooleanValue("Option/ReallyClearOnNextRestart").setValue(options.getBooleanOption("ReallyClearOnNextRestart").getReal()); + } catch (ConfigurationException ce1) { + logger.log(Level.SEVERE, "Could not store configuration!", ce1); + } + } - int replyId = 0; - for (Reply reply : sone.getReplies()) { - String replyPrefix = sonePrefix + "/Reply." + replyId++; - configuration.getStringValue(replyPrefix + "/ID").setValue(reply.getId()); - configuration.getStringValue(replyPrefix + "/Sone/ID").setValue(reply.getSone().getId()); - configuration.getStringValue(replyPrefix + "/Sone/Key").setValue(reply.getSone().getRequestUri().toString()); - configuration.getStringValue(replyPrefix + "/Sone/Name").setValue(reply.getSone().getName()); - configuration.getStringValue(replyPrefix + "/Post").setValue(reply.getPost().getId()); - configuration.getLongValue(replyPrefix + "/Time").setValue(reply.getTime()); - configuration.getStringValue(replyPrefix + "/Text").setValue(reply.getText()); - } - /* write null ID as terminator. */ - configuration.getStringValue(sonePrefix + "/Reply." + replyId + "/ID").setValue(null); - - int friendId = 0; - for (Sone friend : sone.getFriends()) { - String friendPrefix = sonePrefix + "/Friend." + friendId++; - configuration.getStringValue(friendPrefix + "/ID").setValue(friend.getId()); - configuration.getStringValue(friendPrefix + "/Key").setValue(friend.getRequestUri().toString()); - configuration.getStringValue(friendPrefix + "/Name").setValue(friend.getName()); - } - /* write null ID as terminator. */ - configuration.getStringValue(sonePrefix + "/Friend." + friendId + "/ID").setValue(null); + /** + * Generate a Sone URI from the given URI and latest edition. + * + * @param uriString + * The URI to derive the Sone URI from + * @param latestEditionString + * The latest edition as a {@link String}, or {@code null} + * @return The derived URI + */ + private FreenetURI getSoneUri(String uriString, String latestEditionString) { + try { + FreenetURI uri = new FreenetURI(uriString).setDocName("Sone").setMetaString(new String[0]).setSuggestedEdition(Numbers.safeParseLong(latestEditionString, (long) 0)); + return uri; + } catch (MalformedURLException mue1) { + logger.log(Level.WARNING, "Could not create Sone URI from URI: " + uriString, mue1); + return null; + } + } - } - /* write null ID as terminator. */ - configuration.getStringValue("Sone/Sone." + soneId + "/ID").setValue(null); + // + // INTERFACE IdentityListener + // - } catch (ConfigurationException ce1) { - logger.log(Level.WARNING, "Could not store configuration!", ce1); + /** + * {@inheritDoc} + */ + @Override + public void ownIdentityAdded(OwnIdentity ownIdentity) { + logger.log(Level.FINEST, "Adding OwnIdentity: " + ownIdentity); + if (ownIdentity.hasContext("Sone")) { + addLocalSone(ownIdentity); } } + /** + * {@inheritDoc} + */ + @Override + public void ownIdentityRemoved(OwnIdentity ownIdentity) { + /* TODO */ + } + + /** + * {@inheritDoc} + */ + @Override + public void identityAdded(Identity identity) { + logger.log(Level.FINEST, "Adding Identity: " + identity); + addRemoteSone(identity); + } + + /** + * {@inheritDoc} + */ + @Override + public void identityUpdated(Identity identity) { + /* TODO */ + } + + /** + * {@inheritDoc} + */ + @Override + public void identityRemoved(Identity identity) { + /* TODO */ + } + }