X-Git-Url: https://git.pterodactylus.net/?a=blobdiff_plain;f=src%2Fmain%2Fjava%2Fnet%2Fpterodactylus%2Fsone%2Fcore%2FCore.java;h=1a1c6df5dfedc63fec66604c0866b85c3ddde3a8;hb=8e73d78985dbf9b12257bcd1408d17cef98394c6;hp=cf61941aac131122501f94aa2a68fc904d87aee0;hpb=d88fbc4ce31eba494a498c3a2629f30a4a07bedd;p=Sone.git diff --git a/src/main/java/net/pterodactylus/sone/core/Core.java b/src/main/java/net/pterodactylus/sone/core/Core.java index cf61941..1a1c6df 100644 --- a/src/main/java/net/pterodactylus/sone/core/Core.java +++ b/src/main/java/net/pterodactylus/sone/core/Core.java @@ -17,8 +17,10 @@ package net.pterodactylus.sone.core; +import java.io.InputStream; import java.net.MalformedURLException; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -30,13 +32,19 @@ import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; +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.core.SoneException.Type; 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.WebOfTrustConnector; import net.pterodactylus.util.config.Configuration; import net.pterodactylus.util.config.ConfigurationException; +import net.pterodactylus.util.filter.Filter; +import net.pterodactylus.util.filter.Filters; import net.pterodactylus.util.logging.Logging; import net.pterodactylus.util.service.AbstractService; import freenet.client.FetchResult; @@ -49,40 +57,79 @@ import freenet.keys.FreenetURI; */ public class Core extends AbstractService { + /** The default Sones. */ + private static final Set defaultSones = new HashSet(); + + static { + /* Sone of Sone. */ + defaultSones.add("USK@eRHt0ceFsHjRZ11j6dd68RSdIvfd8f9YjJLZ9lnhEyo,iJWjIWh6TkMZm1NY8qBranKTIuwsCPkVPG6T6c6ft-I,AQACAAE/Sone/4"); + /* Sone of Bombe. */ + defaultSones.add("USK@RuW~uAO35Ipne896-1OmaVJNPuYE4ZIB5oZ5ziaU57A,7rV3uiyztXBDt03DCoRiNwiGjgFCJuznM9Okc1opURU,AQACAAE/Sone/29"); + } + + /** + * 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; /** Interface to freenet. */ private FreenetInterface freenetInterface; + /** The WoT connector. */ + private WebOfTrustConnector webOfTrustConnector; + /** The Sone downloader. */ private SoneDownloader soneDownloader; /** The local Sones. */ - private final Set localSones = new HashSet(); + private final Set localSones = Collections.synchronizedSet(new HashSet()); /** Sone inserters. */ - private final Map soneInserters = new HashMap(); + private final Map soneInserters = Collections.synchronizedMap(new HashMap()); + + /** The Sones’ statuses. */ + private final Map soneStatuses = Collections.synchronizedMap(new HashMap()); /* various caches follow here. */ /** Cache for all known Sones. */ - private final Map soneCache = new HashMap(); + private final Map soneCache = Collections.synchronizedMap(new HashMap()); /** Cache for all known posts. */ - private final Map postCache = new HashMap(); + private final Map postCache = Collections.synchronizedMap(new HashMap()); /** Cache for all known replies. */ - private final Map replyCache = new HashMap(); + private final Map replyCache = Collections.synchronizedMap(new HashMap()); /** * Creates a new core. */ public Core() { - super("Sone Core"); + super("Sone Core", false); } // @@ -90,6 +137,15 @@ public class Core extends AbstractService { // /** + * Returns the options of the Sone plugin. + * + * @return The options of the Sone plugin + */ + public Options getOptions() { + return options; + } + + /** * Sets the configuration of the core. * * @param configuration @@ -116,6 +172,27 @@ public class Core extends AbstractService { } /** + * Returns the Web of Trust connector. + * + * @return The Web of Trust connector + */ + public WebOfTrustConnector getWebOfTrustConnector() { + return webOfTrustConnector; + } + + /** + * Sets the Web of Trust connector. + * + * @param webOfTrustConnector + * The Web of Trust connector + * @return This core (for method chaining) + */ + public Core setWebOfTrustConnector(WebOfTrustConnector webOfTrustConnector) { + this.webOfTrustConnector = webOfTrustConnector; + return this; + } + + /** * Returns the local Sones. * * @return The local Sones @@ -135,14 +212,55 @@ public class Core extends AbstractService { public Sone getSone(String soneId) { if (!soneCache.containsKey(soneId)) { Sone sone = new Sone(soneId); - soneDownloader.addSone(sone); soneCache.put(soneId, sone); + setSoneStatus(sone, SoneStatus.unknown); } return soneCache.get(soneId); } /** - * Creates a new post. + * Returns all known sones. + * + * @return All known sones + */ + public Collection getKnownSones() { + return Collections.unmodifiableCollection(soneCache.values()); + } + + /** + * Gets all known Sones that are not local Sones. + * + * @return All remote Sones + */ + public Collection getRemoteSones() { + return Collections.unmodifiableCollection(getKnownSones()); + } + + /** + * 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) { + return soneStatuses.get(sone); + } + + /** + * Sets the status of the Sone. + * + * @param sone + * The Sone to set the status for + * @param soneStatus + * The status of the Sone + */ + public void setSoneStatus(Sone sone, SoneStatus soneStatus) { + soneStatuses.put(sone, soneStatus); + } + + /** + * Creates a new post and adds it to the given Sone. * * @param sone * The sone that creates the post @@ -155,7 +273,7 @@ public class Core extends AbstractService { } /** - * Creates a new post. + * Creates a new post and adds it to the given Sone. * * @param sone * The Sone that creates the post @@ -210,6 +328,20 @@ public class Core extends AbstractService { // /** + * Adds a Sone to watch for updates. The Sone needs to be completely + * initialized. + * + * @param sone + * The Sone to watch for updates + */ + public void addSone(Sone sone) { + soneCache.put(sone.getId(), sone); + if (!localSones.contains(sone)) { + soneDownloader.addSone(sone); + } + } + + /** * Adds the given Sone. * * @param sone @@ -217,9 +349,9 @@ public class Core extends AbstractService { */ public void addLocalSone(Sone sone) { if (localSones.add(sone)) { - SoneInserter soneInserter = new SoneInserter(freenetInterface, sone); + setSoneStatus(sone, SoneStatus.idle); + SoneInserter soneInserter = new SoneInserter(this, freenetInterface, sone); soneInserter.start(); - soneDownloader.removeSone(sone); soneInserters.put(sone, soneInserter); } } @@ -234,7 +366,7 @@ public class Core extends AbstractService { * if a Sone error occurs */ public Sone createSone(String name) throws SoneException { - return createSone(name, null, null); + return createSone(name, "Sone", null, null); } /** @@ -244,6 +376,8 @@ public class Core extends AbstractService { * * @param name * The name of the Sone + * @param documentName + * The document name in the SSK * @param requestUri * The request URI of the Sone, or {@link NullPointerException} * to create a Sone at a random location @@ -254,7 +388,7 @@ public class Core extends AbstractService { * @throws SoneException * if a Sone error occurs */ - public Sone createSone(String name, String requestUri, String insertUri) throws SoneException { + public Sone createSone(String name, String documentName, String requestUri, String insertUri) throws SoneException { if ((name == null) || (name.trim().length() == 0)) { throw new SoneException(Type.INVALID_SONE_NAME); } @@ -271,7 +405,7 @@ public class Core extends AbstractService { Sone sone; try { logger.log(Level.FINEST, "Creating new Sone “%s” at %s (%s)…", new Object[] { name, finalRequestUri, finalInsertUri }); - sone = getSone(UUID.randomUUID().toString()).setName(name).setRequestUri(new FreenetURI(finalRequestUri).setKeyType("USK").setDocName("Sone-" + name)).setInsertUri(new FreenetURI(finalInsertUri).setKeyType("USK").setDocName("Sone-" + name)); + sone = getSone(UUID.randomUUID().toString()).setName(name).setRequestUri(new FreenetURI(finalRequestUri).setKeyType("USK").setDocName(documentName)).setInsertUri(new FreenetURI(finalInsertUri).setKeyType("USK").setDocName(documentName)); sone.setProfile(new Profile()); /* set modification counter to 1 so it is inserted immediately. */ sone.setModificationCounter(1); @@ -283,18 +417,105 @@ public class Core extends AbstractService { } /** - * Loads the Sone from the given request URI. + * Loads the Sone from the given request URI. The fetching of the data is + * performed in a new thread so this method returns immediately. * * @param requestUri * The request URI to load the Sone from */ - public void loadSone(String requestUri) { - try { - FetchResult fetchResult = freenetInterface.fetchUri(new FreenetURI(requestUri).setMetaString(new String[] { "sone.xml" })); - soneDownloader.parseSone(null, fetchResult); - } catch (MalformedURLException mue1) { - logger.log(Level.INFO, "Could not create URI from “" + requestUri + "”.", mue1); + public void loadSone(final String requestUri) { + loadSone(requestUri, null); + } + + /** + * Loads the Sone from the given request URI. The fetching of the data is + * performed in a new thread so this method returns immediately. If + * {@code insertUri} is not {@code null} the loaded Sone is converted into a + * local Sone and available using as any other local Sone. + * + * @param requestUri + * The request URI to load the Sone from + * @param insertUri + * The insert URI of the Sone + */ + public void loadSone(final String requestUri, final String insertUri) { + new Thread(new Runnable() { + + @Override + @SuppressWarnings("synthetic-access") + public void run() { + try { + FreenetURI realRequestUri = new FreenetURI(requestUri).setMetaString(new String[] { "sone.xml" }); + FetchResult fetchResult = freenetInterface.fetchUri(realRequestUri); + if (fetchResult == null) { + return; + } + Sone parsedSone = soneDownloader.parseSone(null, fetchResult, realRequestUri); + if (parsedSone != null) { + if (insertUri != null) { + parsedSone.setInsertUri(new FreenetURI(insertUri)); + addLocalSone(parsedSone); + } else { + addSone(parsedSone); + } + setSoneStatus(parsedSone, SoneStatus.idle); + } + } catch (MalformedURLException mue1) { + logger.log(Level.INFO, "Could not create URI from “" + requestUri + "”.", mue1); + } + } + }, "Sone Downloader").start(); + } + + /** + * Loads a Sone from an input stream. + * + * @param soneInputStream + * The input stream to load the Sone from + * @return The parsed Sone, or {@code null} if the Sone could not be parsed + */ + public Sone loadSone(InputStream soneInputStream) { + Sone parsedSone = soneDownloader.parseSone(soneInputStream); + if (parsedSone == null) { + return null; + } + if (parsedSone.getInsertUri() != null) { + addLocalSone(parsedSone); + } else { + addSone(parsedSone); } + return parsedSone; + } + + /** + * Loads and updates the given Sone. + * + * @param sone + * The Sone to load + */ + public void loadSone(final Sone sone) { + new Thread(new Runnable() { + + @Override + @SuppressWarnings("synthetic-access") + public void run() { + FreenetURI realRequestUri = sone.getRequestUri().setMetaString(new String[] { "sone.xml" }); + setSoneStatus(sone, SoneStatus.downloading); + try { + FetchResult fetchResult = freenetInterface.fetchUri(realRequestUri); + if (fetchResult == null) { + /* TODO - mark Sone as bad. */ + return; + } + Sone parsedSone = soneDownloader.parseSone(sone, fetchResult, realRequestUri); + if (parsedSone != null) { + addSone(parsedSone); + } + } finally { + setSoneStatus(sone, (sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle); + } + } + }, "Sone Downloader").start(); } /** @@ -307,6 +528,8 @@ public class Core extends AbstractService { SoneInserter soneInserter = soneInserters.remove(sone); soneInserter.stop(); localSones.remove(sone); + soneStatuses.remove(sone); + soneCache.remove(sone.getId()); } /** @@ -366,6 +589,52 @@ public class Core extends AbstractService { return replies; } + /** + * Gets all Sones that like the given post. + * + * @param post + * The post to check for + * @return All Sones that like the post + */ + public Collection getLikes(final Post post) { + return Filters.filteredCollection(getKnownSones(), new Filter() { + + @Override + public boolean filterObject(Sone sone) { + return sone.isLikedPostId(post.getId()); + } + }); + } + + /** + * Gets all Sones that like the given reply. + * + * @param reply + * The reply to check for + * @return All Sones that like the reply + */ + public Collection getLikes(final Reply reply) { + return Filters.filteredCollection(getKnownSones(), new Filter() { + + @Override + public boolean filterObject(Sone sone) { + return sone.isLikedReplyId(reply.getId()); + } + }); + } + + /** + * Deletes the given reply. It is removed from its Sone and from the reply + * cache. + * + * @param reply + * The reply to remove + */ + public void deleteReply(Reply reply) { + reply.getSone().removeReply(reply); + replyCache.remove(reply.getId()); + } + // // SERVICE METHODS // @@ -396,11 +665,61 @@ public class Core extends AbstractService { // /** + * Adds some default Sones. + */ + private void addDefaultSones() { + for (String soneUri : defaultSones) { + loadSone(soneUri); + } + } + + /** * Loads the configuration. */ + @SuppressWarnings("unchecked") private void loadConfiguration() { logger.entering(Core.class.getName(), "loadConfiguration()"); + boolean firstStart = configuration.getBooleanValue("FirstStart").getValue(true); + if (firstStart) { + logger.log(Level.INFO, "First start of Sone, adding a couple of default Sones…"); + addDefaultSones(); + try { + configuration.getBooleanValue("FirstStart").setValue(false); + } catch (ConfigurationException ce1) { + logger.log(Level.WARNING, "Could not clear “first start” flag!"); + } + } + + options.addIntegerOption("InsertionDelay", new DefaultOption(60, new OptionWatcher() { + + @Override + public void optionChanged(Option option, Integer oldValue, Integer newValue) { + SoneInserter.setInsertionDelay(newValue); + } + + })); + + options.addBooleanOption("ClearOnNextRestart", new DefaultOption(false)); + options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption(false)); + + if (firstStart) { + return; + } + + 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. */ + addDefaultSones(); + return; + } + + options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null)); + /* parse local Sones. */ logger.log(Level.INFO, "Loading Sones…"); int soneId = 0; @@ -411,18 +730,23 @@ public class Core extends AbstractService { break; } String name = configuration.getStringValue(sonePrefix + "/Name").getValue(null); + long time = configuration.getLongValue(sonePrefix + "/Time").getValue((long) 0); 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); + Integer birthDay = configuration.getIntValue(sonePrefix + "/Profile/BirthDay").getValue(null); + Integer birthMonth = configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null); + Integer birthYear = configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null); try { Profile profile = new Profile(); profile.setFirstName(firstName); profile.setMiddleName(middleName); profile.setLastName(lastName); - Sone sone = getSone(id).setName(name).setRequestUri(new FreenetURI(requestUri)).setInsertUri(new FreenetURI(insertUri)); + profile.setBirthDay(birthDay).setBirthMonth(birthMonth).setBirthYear(birthYear); + Sone sone = getSone(id).setName(name).setTime(time).setRequestUri(new FreenetURI(requestUri)).setInsertUri(new FreenetURI(insertUri)); sone.setProfile(profile); int postId = 0; do { @@ -431,7 +755,7 @@ public class Core extends AbstractService { if (id == null) { break; } - long time = configuration.getLongValue(postPrefix + "/Time").getValue(null); + time = configuration.getLongValue(postPrefix + "/Time").getValue((long) 0); String text = configuration.getStringValue(postPrefix + "/Text").getValue(null); Post post = getPost(id).setSone(sone).setTime(time).setText(text); sone.addPost(post); @@ -462,10 +786,31 @@ public class Core extends AbstractService { String friendKey = configuration.getStringValue(friendPrefix + "/Key").getValue(null); String friendName = configuration.getStringValue(friendPrefix + "/Name").getValue(null); friendSone.setRequestUri(new FreenetURI(friendKey)).setName(friendName); - soneDownloader.addSone(friendSone); sone.addFriend(friendSone); } + /* load liked post IDs. */ + int likedPostIdCounter = 0; + while (true) { + String likedPostIdPrefix = sonePrefix + "/LikedPostId." + likedPostIdCounter++; + String likedPostId = configuration.getStringValue(likedPostIdPrefix + "/ID").getValue(null); + if (likedPostId == null) { + break; + } + sone.addLikedPostId(likedPostId); + } + + /* load liked reply IDs. */ + int likedReplyIdCounter = 0; + while (true) { + String likedReplyIdPrefix = sonePrefix + "/LikedReplyId." + likedReplyIdCounter++; + String likedReplyId = configuration.getStringValue(likedReplyIdPrefix + "/ID").getValue(null); + if (likedReplyId == null) { + break; + } + sone.addLikedReplyId(likedReplyId); + } + sone.setModificationCounter(modificationCounter); addLocalSone(sone); } catch (MalformedURLException mue1) { @@ -474,6 +819,11 @@ public class Core extends AbstractService { } while (true); logger.log(Level.INFO, "Loaded %d Sones.", getSones().size()); + /* load all remote Sones. */ + for (Sone remoteSone : getRemoteSones()) { + loadSone(remoteSone); + } + logger.exiting(Core.class.getName(), "loadConfiguration()"); } @@ -483,13 +833,20 @@ public class Core extends AbstractService { private void saveConfiguration() { Set sones = getSones(); logger.log(Level.INFO, "Storing %d Sones…", sones.size()); + try { + /* store the options first. */ + 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()); + /* 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.getLongValue(sonePrefix + "/Time").setValue(sone.getTime()); configuration.getStringValue(sonePrefix + "/RequestURI").setValue(sone.getRequestUri().toString()); configuration.getStringValue(sonePrefix + "/InsertURI").setValue(sone.getInsertUri().toString()); configuration.getLongValue(sonePrefix + "/ModificationCounter").setValue(sone.getModificationCounter()); @@ -497,6 +854,9 @@ public class Core extends AbstractService { 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()); int postId = 0; for (Post post : sone.getPosts()) { String postPrefix = sonePrefix + "/Post." + postId++; @@ -528,6 +888,22 @@ public class Core extends AbstractService { /* write null ID as terminator. */ configuration.getStringValue(sonePrefix + "/Friend." + friendId + "/ID").setValue(null); + /* write all liked posts. */ + int likedPostIdCounter = 0; + for (String soneLikedPostId : sone.getLikedPostIds()) { + String likedPostIdPrefix = sonePrefix + "/LikedPostId." + likedPostIdCounter++; + configuration.getStringValue(likedPostIdPrefix + "/ID").setValue(soneLikedPostId); + } + configuration.getStringValue(sonePrefix + "/LikedPostId." + likedPostIdCounter + "/ID").setValue(null); + + /* write all liked replies. */ + int likedReplyIdCounter = 0; + for (String soneLikedReplyId : sone.getLikedReplyIds()) { + String likedReplyIdPrefix = sonePrefix + "/LikedReplyId." + likedReplyIdCounter++; + configuration.getStringValue(likedReplyIdPrefix + "/ID").setValue(soneLikedReplyId); + } + configuration.getStringValue(sonePrefix + "/LikedReplyId." + likedReplyIdCounter + "/ID").setValue(null); + } /* write null ID as terminator. */ configuration.getStringValue("Sone/Sone." + soneId + "/ID").setValue(null);