<modelVersion>4.0.0</modelVersion>
<groupId>net.pterodactylus</groupId>
<artifactId>sone</artifactId>
- <version>0.6.1</version>
+ <version>0.6.7</version>
<dependencies>
<dependency>
<groupId>net.pterodactylus</groupId>
<artifactId>utils</artifactId>
- <version>0.9.4-SNAPSHOT</version>
+ <version>0.10.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>junit</groupId>
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.Map.Entry;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.pterodactylus.sone.data.Image;
import net.pterodactylus.sone.data.Post;
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.TemporaryImage;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.fcp.FcpInterface;
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
import net.pterodactylus.sone.freenet.wot.Identity;
import net.pterodactylus.sone.freenet.wot.IdentityListener;
import net.pterodactylus.sone.freenet.wot.IdentityManager;
import net.pterodactylus.util.config.ConfigurationException;
import net.pterodactylus.util.logging.Logging;
import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.service.AbstractService;
+import net.pterodactylus.util.thread.Ticker;
+import net.pterodactylus.util.validation.EqualityValidator;
+import net.pterodactylus.util.validation.IntegerRangeValidator;
+import net.pterodactylus.util.validation.OrValidator;
import net.pterodactylus.util.validation.Validation;
import net.pterodactylus.util.version.Version;
import freenet.keys.FreenetURI;
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
-public class Core implements IdentityListener, UpdateListener, ImageInsertListener {
+public class Core extends AbstractService implements IdentityListener, UpdateListener, SoneProvider, PostProvider, SoneInsertListener, ImageInsertListener {
/**
* Enumeration for the possible states of a {@link Sone}.
/** The image inserter. */
private final ImageInserter imageInserter;
+ /** Sone downloader thread-pool. */
+ private final ExecutorService soneDownloaders = Executors.newFixedThreadPool(10);
+
/** The update checker. */
private final UpdateChecker updateChecker;
- /** Whether the core has been stopped. */
- private volatile boolean stopped;
+ /** The FCP interface. */
+ private volatile FcpInterface fcpInterface;
/** The Sones’ statuses. */
/* synchronize access on itself. */
/* synchronize access on this on localSones. */
private final Map<Sone, SoneInserter> soneInserters = new HashMap<Sone, SoneInserter>();
+ /** Sone rescuers. */
+ /* synchronize access on this on localSones. */
+ private final Map<Sone, SoneRescuer> soneRescuers = new HashMap<Sone, SoneRescuer>();
+
/** All local Sones. */
/* synchronize access on this on itself. */
private Map<String, Sone> localSones = new HashMap<String, Sone>();
/** All temporary images. */
private Map<String, TemporaryImage> temporaryImages = new HashMap<String, TemporaryImage>();
+ /** Ticker for threads that mark own elements as known. */
+ private Ticker localElementTicker = new Ticker();
+
+ /** The time the configuration was last touched. */
+ private volatile long lastConfigurationUpdate;
+
/**
* Creates a new core.
*
* The identity manager
*/
public Core(Configuration configuration, FreenetInterface freenetInterface, IdentityManager identityManager) {
+ super("Sone Core");
this.configuration = configuration;
this.freenetInterface = freenetInterface;
this.identityManager = identityManager;
*/
public void setConfiguration(Configuration configuration) {
this.configuration = configuration;
- saveConfiguration();
+ touchConfiguration();
}
/**
}
/**
+ * Sets the FCP interface to use.
+ *
+ * @param fcpInterface
+ * The FCP interface to use
+ */
+ public void setFcpInterface(FcpInterface fcpInterface) {
+ this.fcpInterface = fcpInterface;
+ }
+
+ /**
* Returns the status of the given Sone.
*
* @param sone
}
/**
+ * 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) {
+ Validation.begin().isNotNull("Sone", sone).check().is("Local Sone", isLocalSone(sone)).check();
+ synchronized (localSones) {
+ 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
* @return The Sone with the given ID, or {@code null} if there is no such
* Sone
*/
+ @Override
public Sone getSone(String id, boolean create) {
if (isLocalSone(id)) {
return getLocalSone(id);
* exists, {@code false} to return {@code null}
* @return The post, or {@code null} if there is no such post
*/
+ @Override
public Post getPost(String postId, boolean create) {
synchronized (posts) {
Post post = posts.get(postId);
*/
public List<Reply> getReplies(Post post) {
Set<Sone> sones = getSones();
+ @SuppressWarnings("hiding")
List<Reply> replies = new ArrayList<Reply>();
for (Sone sone : sones) {
for (Reply reply : sone.getReplies()) {
* @return All bookmarked posts
*/
public Set<Post> getBookmarkedPosts() {
+ @SuppressWarnings("hiding")
Set<Post> posts = new HashSet<Post>();
synchronized (bookmarkedPosts) {
for (String bookmarkedPostId : bookmarkedPosts) {
/* TODO - load posts ’n stuff */
localSones.put(ownIdentity.getId(), sone);
final SoneInserter soneInserter = new SoneInserter(this, freenetInterface, sone);
+ soneInserter.addSoneInsertListener(this);
soneInserters.put(sone, soneInserter);
setSoneStatus(sone, SoneStatus.idle);
loadSone(sone);
- if (!preferences.isSoneRescueMode()) {
- soneInserter.start();
- }
- new Thread(new Runnable() {
-
- @Override
- @SuppressWarnings("synthetic-access")
- public void run() {
- if (!preferences.isSoneRescueMode()) {
- return;
- }
- logger.log(Level.INFO, "Trying to restore Sone from Freenet…");
- coreListenerManager.fireRescuingSone(sone);
- lockSone(sone);
- long edition = sone.getLatestEdition();
- while (!stopped && (edition >= 0) && preferences.isSoneRescueMode()) {
- logger.log(Level.FINE, "Downloading edition " + edition + "…");
- soneDownloader.fetchSone(sone, sone.getRequestUri().setKeyType("SSK").setDocName("Sone-" + edition));
- --edition;
- }
- logger.log(Level.INFO, "Finished restoring Sone from Freenet, starting Inserter…");
- saveSone(sone);
- coreListenerManager.fireRescuedSone(sone);
- soneInserter.start();
- }
-
- }, "Sone Downloader").start();
+ soneInserter.start();
return sone;
}
}
}
Sone sone = addLocalSone(ownIdentity);
sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
- saveSone(sone);
+ sone.addFriend("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI");
+ touchConfiguration();
return sone;
}
for (Sone localSone : getLocalSones()) {
if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
localSone.addFriend(sone.getId());
+ touchConfiguration();
}
}
}
remoteSones.put(identity.getId(), sone);
soneDownloader.addSone(sone);
setSoneStatus(sone, SoneStatus.unknown);
- new Thread(new Runnable() {
+ soneDownloaders.execute(new Runnable() {
@Override
@SuppressWarnings("synthetic-access")
public void run() {
- soneDownloader.fetchSone(sone);
+ soneDownloader.fetchSone(sone, sone.getRequestUri());
}
- }, "Sone Downloader").start();
+ });
return sone;
}
}
}
/**
- * Updates the stores Sone with the given Sone.
+ * 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(Sone sone, boolean soneRescueMode) {
if (hasSone(sone.getId())) {
- boolean soneRescueMode = isLocalSone(sone) && preferences.isSoneRescueMode();
Sone storedSone = getSone(sone.getId());
if (!soneRescueMode && !(sone.getTime() > storedSone.getTime())) {
logger.log(Level.FINE, "Downloaded Sone %s is not newer than stored Sone %s.", new Object[] { sone, storedSone });
return;
}
localSones.remove(sone.getId());
- soneInserters.remove(sone).stop();
+ SoneInserter soneInserter = soneInserters.remove(sone);
+ soneInserter.removeSoneInsertListener(this);
+ soneInserter.stop();
}
try {
((OwnIdentity) sone.getIdentity()).removeContext("Sone");
if (newSones.remove(sone.getId())) {
knownSones.add(sone.getId());
coreListenerManager.fireMarkSoneKnown(sone);
- saveConfiguration();
+ touchConfiguration();
}
}
}
return;
}
+ /* initialize options. */
+ sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
+ sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption<Boolean>(false));
+
/* load Sone. */
String sonePrefix = "Sone/" + sone.getId();
Long soneTime = configuration.getLongValue(sonePrefix + "/Time").getValue(null);
}
/* load posts. */
+ @SuppressWarnings("hiding")
Set<Post> posts = new HashSet<Post>();
while (true) {
String postPrefix = sonePrefix + "/Posts/" + posts.size();
}
/* load replies. */
+ @SuppressWarnings("hiding")
Set<Reply> replies = new HashSet<Reply>();
while (true) {
String replyPrefix = sonePrefix + "/Replies/" + replies.size();
}
/* load options. */
- sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
+ sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").set(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(null));
/* if we’re still here, Sone was loaded successfully. */
synchronized (sone) {
}
/**
- * Saves the given Sone. This will persist all local settings for the given
- * Sone, such as the friends list and similar, private options.
- *
- * @param sone
- * The Sone to save
- */
- public synchronized 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);
- try {
- ((OwnIdentity) sone.getIdentity()).setProperty("Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
-
- /* save Sone into configuration. */
- String sonePrefix = "Sone/" + sone.getId();
- configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
- configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").setValue(soneInserters.get(sone).getLastInsertFingerprint());
-
- /* 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 profile fields. */
- int fieldCounter = 0;
- for (Field profileField : profile.getFields()) {
- String fieldPrefix = sonePrefix + "/Profile/Fields/" + fieldCounter++;
- configuration.getStringValue(fieldPrefix + "/Name").setValue(profileField.getName());
- configuration.getStringValue(fieldPrefix + "/Value").setValue(profileField.getValue());
- }
- configuration.getStringValue(sonePrefix + "/Profile/Fields/" + fieldCounter + "/Name").setValue(null);
-
- /* save posts. */
- int postCounter = 0;
- for (Post post : sone.getPosts()) {
- String postPrefix = sonePrefix + "/Posts/" + postCounter++;
- configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
- configuration.getStringValue(postPrefix + "/Recipient").setValue((post.getRecipient() != null) ? post.getRecipient().getId() : null);
- 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);
-
- /* save friends. */
- int friendCounter = 0;
- for (String friendId : sone.getFriends()) {
- configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter++ + "/ID").setValue(friendId);
- }
- configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
-
- /* save albums. first, collect in a flat structure, top-level first. */
- List<Album> albums = Sone.flattenAlbums(sone.getAlbums());
-
- int albumCounter = 0;
- for (Album album : albums) {
- String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
- configuration.getStringValue(albumPrefix + "/ID").setValue(album.getId());
- configuration.getStringValue(albumPrefix + "/Title").setValue(album.getTitle());
- configuration.getStringValue(albumPrefix + "/Description").setValue(album.getDescription());
- configuration.getStringValue(albumPrefix + "/Parent").setValue(album.getParent() == null ? null : album.getParent().getId());
- }
- configuration.getStringValue(sonePrefix + "/Albums/" + albumCounter + "/ID").setValue(null);
-
- /* save images. */
- int imageCounter = 0;
- for (Album album : albums) {
- for (Image image : album.getImages()) {
- if (!image.isInserted()) {
- continue;
- }
- String imagePrefix = sonePrefix + "/Images/" + imageCounter++;
- configuration.getStringValue(imagePrefix + "/ID").setValue(image.getId());
- configuration.getStringValue(imagePrefix + "/Album").setValue(album.getId());
- configuration.getStringValue(imagePrefix + "/Key").setValue(image.getKey());
- configuration.getStringValue(imagePrefix + "/Title").setValue(image.getTitle());
- configuration.getStringValue(imagePrefix + "/Description").setValue(image.getDescription());
- configuration.getLongValue(imagePrefix + "/CreationTime").setValue(image.getCreationTime());
- configuration.getIntValue(imagePrefix + "/Width").setValue(image.getWidth());
- configuration.getIntValue(imagePrefix + "/Height").setValue(image.getHeight());
- }
- }
- configuration.getStringValue(sonePrefix + "/Images/" + imageCounter + "/ID").setValue(null);
-
- /* save options. */
- configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal());
-
- configuration.save();
- logger.log(Level.INFO, "Sone %s saved.", sone);
- } catch (ConfigurationException ce1) {
- logger.log(Level.WARNING, "Could not save Sone: " + sone, ce1);
- } catch (WebOfTrustException wote1) {
- logger.log(Level.WARNING, "Could not set WoT property for Sone: " + sone, wote1);
- }
- }
-
- /**
* Creates a new post.
*
* @param sone
logger.log(Level.FINE, "Tried to create post for non-local Sone: %s", sone);
return null;
}
- Post post = new Post(sone, time, text);
+ final Post post = new Post(sone, time, text);
if (recipient != null) {
post.setRecipient(recipient);
}
coreListenerManager.fireNewPostFound(post);
}
sone.addPost(post);
- saveSone(sone);
+ touchConfiguration();
+ localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void run() {
+ markPostKnown(post);
+ }
+ }, "Mark " + post + " read.");
return post;
}
synchronized (posts) {
posts.remove(post.getId());
}
+ coreListenerManager.firePostRemoved(post);
synchronized (newPosts) {
markPostKnown(post);
knownPosts.remove(post.getId());
}
- saveSone(post.getSone());
+ touchConfiguration();
}
/**
if (newPosts.remove(post.getId())) {
knownPosts.add(post.getId());
coreListenerManager.fireMarkPostKnown(post);
- saveConfiguration();
+ touchConfiguration();
}
}
}
logger.log(Level.FINE, "Tried to create reply for non-local Sone: %s", sone);
return null;
}
- Reply reply = new Reply(sone, post, System.currentTimeMillis(), text);
+ final Reply reply = new Reply(sone, post, System.currentTimeMillis(), text);
synchronized (replies) {
replies.put(reply.getId(), reply);
}
coreListenerManager.fireNewReplyFound(reply);
}
sone.addReply(reply);
- saveSone(sone);
+ touchConfiguration();
+ localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void run() {
+ markReplyKnown(reply);
+ }
+ }, "Mark " + reply + " read.");
return reply;
}
knownReplies.remove(reply.getId());
}
sone.removeReply(reply);
- saveSone(sone);
+ touchConfiguration();
}
/**
if (newReplies.remove(reply.getId())) {
knownReplies.add(reply.getId());
coreListenerManager.fireMarkReplyKnown(reply);
- saveConfiguration();
+ touchConfiguration();
}
}
}
}
/**
+ * Notifies the core that the configuration, either of the core or of a
+ * single local Sone, has changed, and that the configuration should be
+ * saved.
+ */
+ public void touchConfiguration() {
+ lastConfigurationUpdate = System.currentTimeMillis();
+ }
+
+ //
+ // SERVICE METHODS
+ //
+
+ /**
* Starts the core.
*/
- public void start() {
+ @Override
+ public void serviceStart() {
loadConfiguration();
updateChecker.addUpdateListener(this);
updateChecker.start();
}
/**
+ * {@inheritDoc}
+ */
+ @Override
+ public void serviceRun() {
+ long lastSaved = System.currentTimeMillis();
+ while (!shouldStop()) {
+ sleep(1000);
+ long now = System.currentTimeMillis();
+ if (shouldStop() || ((lastConfigurationUpdate > lastSaved) && ((now - lastConfigurationUpdate) > 5000))) {
+ for (Sone localSone : getLocalSones()) {
+ saveSone(localSone);
+ }
+ saveConfiguration();
+ lastSaved = now;
+ }
+ }
+ }
+
+ /**
* Stops the core.
*/
- public void stop() {
+ @Override
+ public void serviceStop() {
synchronized (localSones) {
for (SoneInserter soneInserter : soneInserters.values()) {
+ soneInserter.removeSoneInsertListener(this);
soneInserter.stop();
}
}
updateChecker.stop();
updateChecker.removeUpdateListener(this);
soneDownloader.stop();
- saveConfiguration();
- stopped = true;
+ }
+
+ //
+ // PRIVATE METHODS
+ //
+
+ /**
+ * Saves the given Sone. This will persist all local settings for the given
+ * Sone, such as the friends list and similar, private options.
+ *
+ * @param sone
+ * The Sone to save
+ */
+ private synchronized 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);
+ try {
+ ((OwnIdentity) sone.getIdentity()).setProperty("Sone.LatestEdition", String.valueOf(sone.getLatestEdition()));
+
+ /* save Sone into configuration. */
+ String sonePrefix = "Sone/" + sone.getId();
+ configuration.getLongValue(sonePrefix + "/Time").setValue(sone.getTime());
+ configuration.getStringValue(sonePrefix + "/LastInsertFingerprint").setValue(soneInserters.get(sone).getLastInsertFingerprint());
+
+ /* 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 profile fields. */
+ int fieldCounter = 0;
+ for (Field profileField : profile.getFields()) {
+ String fieldPrefix = sonePrefix + "/Profile/Fields/" + fieldCounter++;
+ configuration.getStringValue(fieldPrefix + "/Name").setValue(profileField.getName());
+ configuration.getStringValue(fieldPrefix + "/Value").setValue(profileField.getValue());
+ }
+ configuration.getStringValue(sonePrefix + "/Profile/Fields/" + fieldCounter + "/Name").setValue(null);
+
+ /* save posts. */
+ int postCounter = 0;
+ for (Post post : sone.getPosts()) {
+ String postPrefix = sonePrefix + "/Posts/" + postCounter++;
+ configuration.getStringValue(postPrefix + "/ID").setValue(post.getId());
+ configuration.getStringValue(postPrefix + "/Recipient").setValue((post.getRecipient() != null) ? post.getRecipient().getId() : null);
+ 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);
+
+ /* save friends. */
+ int friendCounter = 0;
+ for (String friendId : sone.getFriends()) {
+ configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter++ + "/ID").setValue(friendId);
+ }
+ configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
+
+ /* save albums. first, collect in a flat structure, top-level first. */
+ List<Album> albums = Sone.flattenAlbums(sone.getAlbums());
+
+ int albumCounter = 0;
+ for (Album album : albums) {
+ String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
+ configuration.getStringValue(albumPrefix + "/ID").setValue(album.getId());
+ configuration.getStringValue(albumPrefix + "/Title").setValue(album.getTitle());
+ configuration.getStringValue(albumPrefix + "/Description").setValue(album.getDescription());
+ configuration.getStringValue(albumPrefix + "/Parent").setValue(album.getParent() == null ? null : album.getParent().getId());
+ }
+ configuration.getStringValue(sonePrefix + "/Albums/" + albumCounter + "/ID").setValue(null);
+
+ /* save images. */
+ int imageCounter = 0;
+ for (Album album : albums) {
+ for (Image image : album.getImages()) {
+ if (!image.isInserted()) {
+ continue;
+ }
+ String imagePrefix = sonePrefix + "/Images/" + imageCounter++;
+ configuration.getStringValue(imagePrefix + "/ID").setValue(image.getId());
+ configuration.getStringValue(imagePrefix + "/Album").setValue(album.getId());
+ configuration.getStringValue(imagePrefix + "/Key").setValue(image.getKey());
+ configuration.getStringValue(imagePrefix + "/Title").setValue(image.getTitle());
+ configuration.getStringValue(imagePrefix + "/Description").setValue(image.getDescription());
+ configuration.getLongValue(imagePrefix + "/CreationTime").setValue(image.getCreationTime());
+ configuration.getIntValue(imagePrefix + "/Width").setValue(image.getWidth());
+ configuration.getIntValue(imagePrefix + "/Height").setValue(image.getHeight());
+ }
+ }
+ configuration.getStringValue(sonePrefix + "/Images/" + imageCounter + "/ID").setValue(null);
+
+ /* save options. */
+ configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal());
+ configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").setValue(sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").getReal());
+
+ configuration.save();
+ logger.log(Level.INFO, "Sone %s saved.", sone);
+ } catch (ConfigurationException ce1) {
+ logger.log(Level.WARNING, "Could not save Sone: " + sone, ce1);
+ } catch (WebOfTrustException wote1) {
+ logger.log(Level.WARNING, "Could not set WoT property for Sone: " + sone, wote1);
+ }
}
/**
* Saves the current options.
*/
- public void saveConfiguration() {
+ private void saveConfiguration() {
synchronized (configuration) {
if (storingConfiguration) {
logger.log(Level.FINE, "Already storing configuration…");
configuration.getIntValue("Option/ConfigurationVersion").setValue(0);
configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
configuration.getIntValue("Option/PostsPerPage").setValue(options.getIntegerOption("PostsPerPage").getReal());
+ configuration.getIntValue("Option/CharactersPerPost").setValue(options.getIntegerOption("CharactersPerPost").getReal());
+ configuration.getBooleanValue("Option/RequireFullAccess").setValue(options.getBooleanOption("RequireFullAccess").getReal());
configuration.getIntValue("Option/PositiveTrust").setValue(options.getIntegerOption("PositiveTrust").getReal());
configuration.getIntValue("Option/NegativeTrust").setValue(options.getIntegerOption("NegativeTrust").getReal());
configuration.getStringValue("Option/TrustComment").setValue(options.getStringOption("TrustComment").getReal());
+ configuration.getBooleanValue("Option/ActivateFcpInterface").setValue(options.getBooleanOption("ActivateFcpInterface").getReal());
+ configuration.getIntValue("Option/FcpFullAccessRequired").setValue(options.getIntegerOption("FcpFullAccessRequired").getReal());
configuration.getBooleanValue("Option/SoneRescueMode").setValue(options.getBooleanOption("SoneRescueMode").getReal());
configuration.getBooleanValue("Option/ClearOnNextRestart").setValue(options.getBooleanOption("ClearOnNextRestart").getReal());
configuration.getBooleanValue("Option/ReallyClearOnNextRestart").setValue(options.getBooleanOption("ReallyClearOnNextRestart").getReal());
}
}
- //
- // PRIVATE METHODS
- //
-
/**
* Loads the configuration.
*/
@SuppressWarnings("unchecked")
private void loadConfiguration() {
/* create options. */
- options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new OptionWatcher<Integer>() {
+ options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new IntegerRangeValidator(0, Integer.MAX_VALUE), new OptionWatcher<Integer>() {
@Override
public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
}
}));
- options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10));
- options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75));
- options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25));
+ options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
+ options.addIntegerOption("CharactersPerPost", new DefaultOption<Integer>(200, new OrValidator<Integer>(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator<Integer>(-1))));
+ options.addBooleanOption("RequireFullAccess", new DefaultOption<Boolean>(false));
+ options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75, new IntegerRangeValidator(0, 100)));
+ options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25, new IntegerRangeValidator(-100, 100)));
options.addStringOption("TrustComment", new DefaultOption<String>("Set from Sone Web Interface"));
+ options.addBooleanOption("ActivateFcpInterface", new DefaultOption<Boolean>(false, new OptionWatcher<Boolean>() {
+
+ @Override
+ @SuppressWarnings("synthetic-access")
+ public void optionChanged(Option<Boolean> option, Boolean oldValue, Boolean newValue) {
+ fcpInterface.setActive(newValue);
+ }
+ }));
+ options.addIntegerOption("FcpFullAccessRequired", new DefaultOption<Integer>(2, new OptionWatcher<Integer>() {
+
+ @Override
+ @SuppressWarnings("synthetic-access")
+ public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
+ fcpInterface.setFullAccessRequired(FullAccessRequired.values()[newValue]);
+ }
+
+ }));
options.addBooleanOption("SoneRescueMode", new DefaultOption<Boolean>(false));
options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false));
options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption<Boolean>(false));
return;
}
- options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null));
- options.getIntegerOption("PostsPerPage").set(configuration.getIntValue("Option/PostsPerPage").getValue(null));
- options.getIntegerOption("PositiveTrust").set(configuration.getIntValue("Option/PositiveTrust").getValue(null));
- options.getIntegerOption("NegativeTrust").set(configuration.getIntValue("Option/NegativeTrust").getValue(null));
+ loadConfigurationValue("InsertionDelay");
+ loadConfigurationValue("PostsPerPage");
+ loadConfigurationValue("CharactersPerPost");
+ options.getBooleanOption("RequireFullAccess").set(configuration.getBooleanValue("Option/RequireFullAccess").getValue(null));
+ loadConfigurationValue("PositiveTrust");
+ loadConfigurationValue("NegativeTrust");
options.getStringOption("TrustComment").set(configuration.getStringValue("Option/TrustComment").getValue(null));
+ options.getBooleanOption("ActivateFcpInterface").set(configuration.getBooleanValue("Option/ActivateFcpInterface").getValue(null));
+ options.getIntegerOption("FcpFullAccessRequired").set(configuration.getIntValue("Option/FcpFullAccessRequired").getValue(null));
options.getBooleanOption("SoneRescueMode").set(configuration.getBooleanValue("Option/SoneRescueMode").getValue(null));
/* load known Sones. */
}
/**
+ * Loads an {@link Integer} configuration value for the option with the
+ * given name, logging validation failures.
+ *
+ * @param optionName
+ * The name of the option to load
+ */
+ private void loadConfigurationValue(String optionName) {
+ try {
+ options.getIntegerOption(optionName).set(configuration.getIntValue("Option/" + optionName).getValue(null));
+ } catch (IllegalArgumentException iae1) {
+ logger.log(Level.WARNING, "Invalid value for " + optionName + " in configuration, using default.");
+ }
+ }
+
+ /**
* Generate a Sone URI from the given URI and latest edition.
*
* @param uriString
public void run() {
Sone sone = getRemoteSone(identity.getId());
sone.setIdentity(identity);
+ sone.setLatestEdition(Numbers.safeParseLong(identity.getProperty("Sone.LatestEdition"), sone.getLatestEdition()));
soneDownloader.addSone(sone);
soneDownloader.fetchSone(sone);
}
@Override
public void identityRemoved(OwnIdentity ownIdentity, Identity identity) {
trustedIdentities.get(ownIdentity).remove(identity);
+ boolean foundIdentity = false;
+ for (Entry<OwnIdentity, Set<Identity>> trustedIdentity : trustedIdentities.entrySet()) {
+ if (trustedIdentity.getKey().equals(ownIdentity)) {
+ continue;
+ }
+ if (trustedIdentity.getValue().contains(identity)) {
+ foundIdentity = true;
+ }
+ }
+ if (foundIdentity) {
+ /* some local identity still trusts this identity, don’t remove. */
+ return;
+ }
+ Sone sone = getSone(identity.getId(), false);
+ if (sone == null) {
+ /* TODO - we don’t have the Sone anymore. should this happen? */
+ return;
+ }
+ synchronized (posts) {
+ synchronized (newPosts) {
+ for (Post post : sone.getPosts()) {
+ posts.remove(post.getId());
+ newPosts.remove(post.getId());
+ coreListenerManager.firePostRemoved(post);
+ }
+ }
+ }
+ synchronized (replies) {
+ synchronized (newReplies) {
+ for (Reply reply : sone.getReplies()) {
+ replies.remove(reply.getId());
+ newReplies.remove(reply.getId());
+ coreListenerManager.fireReplyRemoved(reply);
+ }
+ }
+ }
+ synchronized (remoteSones) {
+ remoteSones.remove(identity.getId());
+ }
+ synchronized (newSones) {
+ newSones.remove(identity.getId());
+ coreListenerManager.fireSoneRemoved(sone);
+ }
}
//
* {@inheritDoc}
*/
@Override
+ public void insertStarted(Sone sone) {
+ coreListenerManager.fireSoneInserting(sone);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void insertFinished(Sone sone, long insertDuration) {
+ coreListenerManager.fireSoneInserted(sone, insertDuration);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void insertAborted(Sone sone, Throwable cause) {
+ coreListenerManager.fireSoneInsertAborted(sone, cause);
+ }
+
+ //
+ // SONEINSERTLISTENER METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
public void imageInsertStarted(Image image) {
logger.log(Level.WARNING, "Image insert started for " + image);
coreListenerManager.fireImageInsertStarted(image);
}
/**
+ * Validates the given insertion delay.
+ *
+ * @param insertionDelay
+ * The insertion delay to validate
+ * @return {@code true} if the given insertion delay was valid, {@code
+ * false} otherwise
+ */
+ public boolean validateInsertionDelay(Integer insertionDelay) {
+ return options.getIntegerOption("InsertionDelay").validate(insertionDelay);
+ }
+
+ /**
* Sets the insertion delay
*
* @param insertionDelay
}
/**
+ * Validates the number of posts per page.
+ *
+ * @param postsPerPage
+ * The number of posts per page
+ * @return {@code true} if the number of posts per page was valid,
+ * {@code false} otherwise
+ */
+ public boolean validatePostsPerPage(Integer postsPerPage) {
+ return options.getIntegerOption("PostsPerPage").validate(postsPerPage);
+ }
+
+ /**
* Sets the number of posts to show per page.
*
* @param postsPerPage
}
/**
+ * Returns the number of characters per post, or <code>-1</code> if the
+ * posts should not be cut off.
+ *
+ * @return The numbers of characters per post
+ */
+ public int getCharactersPerPost() {
+ return options.getIntegerOption("CharactersPerPost").get();
+ }
+
+ /**
+ * Validates the number of characters per post.
+ *
+ * @param charactersPerPost
+ * The number of characters per post
+ * @return {@code true} if the number of characters per post was valid,
+ * {@code false} otherwise
+ */
+ public boolean validateCharactersPerPost(Integer charactersPerPost) {
+ return options.getIntegerOption("CharactersPerPost").validate(charactersPerPost);
+ }
+
+ /**
+ * Sets the number of characters per post.
+ *
+ * @param charactersPerPost
+ * The number of characters per post, or <code>-1</code> to
+ * not cut off the posts
+ * @return This preferences objects
+ */
+ public Preferences setCharactersPerPost(Integer charactersPerPost) {
+ options.getIntegerOption("CharactersPerPost").set(charactersPerPost);
+ return this;
+ }
+
+ /**
+ * Returns whether Sone requires full access to be even visible.
+ *
+ * @return {@code true} if Sone requires full access, {@code false}
+ * otherwise
+ */
+ public boolean isRequireFullAccess() {
+ return options.getBooleanOption("RequireFullAccess").get();
+ }
+
+ /**
+ * Sets whether Sone requires full access to be even visible.
+ *
+ * @param requireFullAccess
+ * {@code true} if Sone requires full access, {@code false}
+ * otherwise
+ */
+ public void setRequireFullAccess(Boolean requireFullAccess) {
+ options.getBooleanOption("RequireFullAccess").set(requireFullAccess);
+ }
+
+ /**
* Returns the positive trust.
*
* @return The positive trust
}
/**
+ * Validates the positive trust.
+ *
+ * @param positiveTrust
+ * The positive trust to validate
+ * @return {@code true} if the positive trust was valid, {@code false}
+ * otherwise
+ */
+ public boolean validatePositiveTrust(Integer positiveTrust) {
+ return options.getIntegerOption("PositiveTrust").validate(positiveTrust);
+ }
+
+ /**
* Sets the positive trust.
*
* @param positiveTrust
}
/**
+ * Validates the negative trust.
+ *
+ * @param negativeTrust
+ * The negative trust to validate
+ * @return {@code true} if the negative trust was valid, {@code false}
+ * otherwise
+ */
+ public boolean validateNegativeTrust(Integer negativeTrust) {
+ return options.getIntegerOption("NegativeTrust").validate(negativeTrust);
+ }
+
+ /**
* Sets the negative trust.
*
* @param negativeTrust
}
/**
- * Returns whether the rescue mode is active.
+ * Returns whether the {@link FcpInterface FCP interface} is currently
+ * active.
*
- * @return {@code true} if the rescue mode is active, {@code false}
- * otherwise
+ * @see FcpInterface#setActive(boolean)
+ * @return {@code true} if the FCP interface is currently active,
+ * {@code false} otherwise
*/
- public boolean isSoneRescueMode() {
- return options.getBooleanOption("SoneRescueMode").get();
+ public boolean isFcpInterfaceActive() {
+ return options.getBooleanOption("ActivateFcpInterface").get();
}
/**
- * Sets whether the rescue mode is active.
+ * Sets whether the {@link FcpInterface FCP interface} is currently
+ * active.
*
- * @param soneRescueMode
- * {@code true} if the rescue mode is active, {@code false}
- * otherwise
+ * @see FcpInterface#setActive(boolean)
+ * @param fcpInterfaceActive
+ * {@code true} to activate the FCP interface, {@code false}
+ * to deactivate the FCP interface
+ * @return This preferences object
+ */
+ public Preferences setFcpInterfaceActive(boolean fcpInterfaceActive) {
+ options.getBooleanOption("ActivateFcpInterface").set(fcpInterfaceActive);
+ return this;
+ }
+
+ /**
+ * Returns the action level for which full access to the FCP interface
+ * is required.
+ *
+ * @return The action level for which full access to the FCP interface
+ * is required
+ */
+ public FullAccessRequired getFcpFullAccessRequired() {
+ return FullAccessRequired.values()[options.getIntegerOption("FcpFullAccessRequired").get()];
+ }
+
+ /**
+ * Sets the action level for which full access to the FCP interface is
+ * required
+ *
+ * @param fcpFullAccessRequired
+ * The action level
* @return This preferences
*/
- public Preferences setSoneRescueMode(Boolean soneRescueMode) {
- options.getBooleanOption("SoneRescueMode").set(soneRescueMode);
+ public Preferences setFcpFullAccessRequired(FullAccessRequired fcpFullAccessRequired) {
+ options.getIntegerOption("FcpFullAccessRequired").set((fcpFullAccessRequired != null) ? fcpFullAccessRequired.ordinal() : null);
return this;
}
public interface CoreListener extends EventListener {
/**
- * Notifies a listener that a Sone is now being rescued.
- *
- * @param sone
- * The Sone that is rescued
- */
- public void rescuingSone(Sone sone);
-
- /**
- * Notifies a listener that the Sone was rescued and can now be unlocked.
- *
- * @param sone
- * The Sone that was rescued
- */
- public void rescuedSone(Sone sone);
-
- /**
* Notifies a listener that a new Sone has been discovered.
*
* @param sone
public void markReplyKnown(Reply reply);
/**
+ * Notifies a listener that the given Sone was removed.
+ *
+ * @param sone
+ * The removed Sone
+ */
+ public void soneRemoved(Sone sone);
+
+ /**
* Notifies a listener that the given post was removed.
*
* @param post
public void soneUnlocked(Sone sone);
/**
+ * Notifies a listener that the insert of the given Sone has started.
+ *
+ * @see SoneInsertListener#insertStarted(Sone)
+ * @param sone
+ * The Sone that is being inserted
+ */
+ public void soneInserting(Sone sone);
+
+ /**
+ * Notifies a listener that the insert of the given Sone has finished
+ * successfully.
+ *
+ * @see SoneInsertListener#insertFinished(Sone, long)
+ * @param sone
+ * The Sone that has been inserted
+ * @param insertDuration
+ * The insert duration (in milliseconds)
+ */
+ public void soneInserted(Sone sone, long insertDuration);
+
+ /**
+ * Notifies a listener that the insert of the given Sone was aborted.
+ *
+ * @see SoneInsertListener#insertAborted(Sone, Throwable)
+ * @param sone
+ * The Sone that was inserted
+ * @param cause
+ * The cause for the abortion (may be {@code null})
+ */
+ public void soneInsertAborted(Sone sone, Throwable cause);
+
+ /**
* Notifies a listener that a new version has been found.
*
* @param version
//
/**
- * Notifies all listeners that the given Sone is now being rescued.
- *
- * @see CoreListener#rescuingSone(Sone)
- * @param sone
- * The Sone that is being rescued
- */
- void fireRescuingSone(Sone sone) {
- for (CoreListener coreListener : getListeners()) {
- coreListener.rescuingSone(sone);
- }
- }
-
- /**
- * Notifies all listeners that the given Sone was rescued.
- *
- * @see CoreListener#rescuedSone(Sone)
- * @param sone
- * The Sone that was rescued
- */
- void fireRescuedSone(Sone sone) {
- for (CoreListener coreListener : getListeners()) {
- coreListener.rescuedSone(sone);
- }
- }
-
- /**
* Notifies all listeners that a new Sone has been discovered.
*
* @see CoreListener#newSoneFound(Sone)
}
/**
+ * Notifies all listener that the given Sone was removed.
+ *
+ * @see CoreListener#soneRemoved(Sone)
+ * @param sone
+ * The removed Sone
+ */
+ void fireSoneRemoved(Sone sone) {
+ for (CoreListener coreListener : getListeners()) {
+ coreListener.soneRemoved(sone);
+ }
+ }
+
+ /**
* Notifies all listener that the given post was removed.
*
* @see CoreListener#postRemoved(Post)
}
/**
+ * Notifies all listeners that the insert of the given Sone has started.
+ *
+ * @see SoneInsertListener#insertStarted(Sone)
+ * @param sone
+ * The Sone being inserted
+ */
+ void fireSoneInserting(Sone sone) {
+ for (CoreListener coreListener : getListeners()) {
+ coreListener.soneInserting(sone);
+ }
+ }
+
+ /**
+ * Notifies all listeners that the insert of the given Sone has finished
+ * successfully.
+ *
+ * @see SoneInsertListener#insertFinished(Sone, long)
+ * @param sone
+ * The Sone that was inserted
+ * @param insertDuration
+ * The insert duration (in milliseconds)
+ */
+ void fireSoneInserted(Sone sone, long insertDuration) {
+ for (CoreListener coreListener : getListeners()) {
+ coreListener.soneInserted(sone, insertDuration);
+ }
+ }
+
+ /**
+ * Notifies all listeners that the insert of the given Sone was aborted.
+ *
+ * @see SoneInsertListener#insertStarted(Sone)
+ * @param sone
+ * The Sone being inserted
+ * @param cause
+ * The cause for the abortion (may be {@code null}
+ */
+ void fireSoneInsertAborted(Sone sone, Throwable cause) {
+ for (CoreListener coreListener : getListeners()) {
+ coreListener.soneInsertAborted(sone, cause);
+ }
+ }
+
+ /**
* Notifies all listeners that a new version was found.
*
* @see CoreListener#updateFound(Version, long, long)
/*
- * FreenetSone - FreenetInterface.java - Copyright © 2010 David Roden
+ * Sone - FreenetInterface.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
+/*
+ * Sone - Options.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
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
package net.pterodactylus.sone.core;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import net.pterodactylus.util.validation.Validator;
+
/**
* Stores various options that influence Sone’s behaviour.
*
public T getReal();
/**
+ * Validates the given value. Note that {@code null} is always a valid
+ * value!
+ *
+ * @param value
+ * The value to validate
+ * @return {@code true} if this option does not have a {@link Validator}
+ * , or the {@link Validator} validates this object, {@code
+ * false} otherwise
+ */
+ public boolean validate(T value);
+
+ /**
* Sets the current value of the option.
*
* @param value
* The new value of the option
+ * @throws IllegalArgumentException
+ * if the value is not valid for this option
*/
- public void set(T value);
+ public void set(T value) throws IllegalArgumentException;
}
/** The current value. */
private volatile T value;
+ /** The validator. */
+ private Validator<T> validator;
+
/** The option watcher. */
private final List<OptionWatcher<T>> optionWatchers = new ArrayList<OptionWatcher<T>>();
* The option watchers
*/
public DefaultOption(T defaultValue, OptionWatcher<T>... optionWatchers) {
+ this(defaultValue, null, optionWatchers);
+ }
+
+ /**
+ * Creates a new default option.
+ *
+ * @param defaultValue
+ * The default value of the option
+ * @param validator
+ * The validator for value validation
+ * @param optionWatchers
+ * The option watchers
+ */
+ public DefaultOption(T defaultValue, Validator<T> validator, OptionWatcher<T>... optionWatchers) {
this.defaultValue = defaultValue;
+ this.validator = validator;
this.optionWatchers.addAll(Arrays.asList(optionWatchers));
}
* {@inheritDoc}
*/
@Override
+ public boolean validate(@SuppressWarnings("hiding") T value) {
+ return (validator == null) || (value == null) || validator.validate(value);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
public void set(T value) {
+ if ((value != null) && (validator != null) && (!validator.validate(value))) {
+ throw new IllegalArgumentException("New Value (" + value + ") could not be validated.");
+ }
T oldValue = this.value;
this.value = value;
if (!get().equals(oldValue)) {
--- /dev/null
+/*
+ * Sone - PostProvider.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core;
+
+import net.pterodactylus.sone.data.Post;
+
+/**
+ * Interface for objects that can provide {@link Post}s by their ID.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface PostProvider {
+
+ /**
+ * Returns the post with the given ID, if it exists. If it does not exist
+ * and {@code create} is {@code false}, {@code null} is returned; otherwise,
+ * a new post with the given ID is created and returned.
+ *
+ * @param postId
+ * The ID of the post to return
+ * @param create
+ * {@code true} to create a new post if no post with the given ID
+ * exists, {@code false} to return {@code null} instead
+ * @return The post with the given ID, or {@code null}
+ */
+ public Post getPost(String postId, boolean create);
+
+}
import java.util.logging.Level;
import java.util.logging.Logger;
-import net.pterodactylus.sone.core.Core.Preferences;
import net.pterodactylus.sone.core.Core.SoneStatus;
import net.pterodactylus.sone.data.Album;
import net.pterodactylus.sone.data.Client;
* The Sone to fetch
*/
public void fetchSone(Sone sone) {
- fetchSone(sone, sone.getRequestUri());
+ fetchSone(sone, sone.getRequestUri().sskForUSK());
}
/**
* Fetches the updated Sone. This method can be used to fetch a Sone from a
- * specific URI (which happens when {@link Preferences#isSoneRescueMode()
- * „Sone rescue mode“} is active).
+ * specific URI.
*
* @param sone
* The Sone to fetch
* The URI to fetch the Sone from
*/
public void fetchSone(Sone sone, FreenetURI soneUri) {
+ fetchSone(sone, soneUri, false);
+ }
+
+ /**
+ * Fetches the Sone from the given URI.
+ *
+ * @param sone
+ * The Sone to fetch
+ * @param soneUri
+ * The URI of the Sone to fetch
+ * @param fetchOnly
+ * {@code true} to only fetch and parse the Sone, {@code false}
+ * to {@link Core#updateSone(Sone) update} it in the core
+ * @return The downloaded Sone, or {@code null} if the Sone could not be
+ * downloaded
+ */
+ public Sone fetchSone(Sone sone, FreenetURI soneUri, boolean fetchOnly) {
logger.log(Level.FINE, "Starting fetch for Sone “%s” from %s…", new Object[] { sone, soneUri });
FreenetURI requestUri = soneUri.setMetaString(new String[] { "sone.xml" });
core.setSoneStatus(sone, SoneStatus.downloading);
Pair<FreenetURI, FetchResult> fetchResults = freenetInterface.fetchUri(requestUri);
if (fetchResults == null) {
/* TODO - mark Sone as bad. */
- return;
+ return null;
}
logger.log(Level.FINEST, "Got %d bytes back.", fetchResults.getRight().size());
Sone parsedSone = parseSone(sone, fetchResults.getRight(), fetchResults.getLeft());
if (parsedSone != null) {
- addSone(parsedSone);
- core.updateSone(parsedSone);
+ if (!fetchOnly) {
+ core.updateSone(parsedSone);
+ addSone(parsedSone);
+ }
}
+ return parsedSone;
} finally {
core.setSoneStatus(sone, (sone.getTime() == 0) ? SoneStatus.unknown : SoneStatus.idle);
}
/*
- * FreenetSone - SoneException.java - Copyright © 2010 David Roden
+ * Sone - SoneException.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
--- /dev/null
+/*
+ * Sone - SoneInsertListener.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core;
+
+import java.util.EventListener;
+
+import net.pterodactylus.sone.data.Sone;
+
+/**
+ * Listener for Sone insert events.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface SoneInsertListener extends EventListener {
+
+ /**
+ * Notifies a listener that a Sone is now being inserted.
+ *
+ * @param sone
+ * The Sone being inserted
+ */
+ public void insertStarted(Sone sone);
+
+ /**
+ * Notifies a listener that a Sone has been successfully inserted.
+ *
+ * @param sone
+ * The Sone that was inserted
+ * @param insertDuration
+ * The duration of the insert (in milliseconds)
+ */
+ public void insertFinished(Sone sone, long insertDuration);
+
+ /**
+ * Notifies a listener that the insert of the given Sone was aborted.
+ *
+ * @param sone
+ * The Sone that was being inserted
+ * @param cause
+ * The cause of the abortion (may be {@code null})
+ */
+ public void insertAborted(Sone sone, Throwable cause);
+
+}
--- /dev/null
+/*
+ * Sone - SoneInsertListenerManager.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.event.AbstractListenerManager;
+
+/**
+ * Manager for {@link SoneInsertListener}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneInsertListenerManager extends AbstractListenerManager<Sone, SoneInsertListener> {
+
+ /**
+ * Creates a new Sone insert listener manager.
+ *
+ * @param sone
+ * The sone being inserted
+ */
+ public SoneInsertListenerManager(Sone sone) {
+ super(sone);
+ }
+
+ //
+ // ACTIONS
+ //
+
+ /**
+ * Notifies all listeners that the insert of the Sone has started.
+ *
+ * @see SoneInsertListener#insertStarted(Sone)
+ */
+ void fireInsertStarted() {
+ for (SoneInsertListener soneInsertListener : getListeners()) {
+ soneInsertListener.insertStarted(getSource());
+ }
+ }
+
+ /**
+ * Notifies all listeners that the insert of the Sone has finished
+ * successfully.
+ *
+ * @see SoneInsertListener#insertFinished(Sone, long)
+ * @param insertDuration
+ * The insert duration (in milliseconds)
+ */
+ void fireInsertFinished(long insertDuration) {
+ for (SoneInsertListener soneInsertListener : getListeners()) {
+ soneInsertListener.insertFinished(getSource(), insertDuration);
+ }
+ }
+
+ /**
+ * Notifies all listeners that the insert of the Sone was aborted.
+ *
+ * @see SoneInsertListener#insertAborted(Sone, Throwable)
+ * @param cause
+ * The cause of the abortion (may be {@code null}
+ */
+ void fireInsertAborted(Throwable cause) {
+ for (SoneInsertListener soneInsertListener : getListeners()) {
+ soneInsertListener.insertAborted(getSource(), cause);
+ }
+ }
+
+}
/*
- * FreenetSone - SoneInserter.java - Copyright © 2010 David Roden
+ * Sone - SoneInserter.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
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.freenet.StringBucket;
import net.pterodactylus.sone.main.SonePlugin;
+import net.pterodactylus.util.collection.ListBuilder;
+import net.pterodactylus.util.collection.ReverseComparator;
import net.pterodactylus.util.io.Closer;
import net.pterodactylus.util.logging.Logging;
import net.pterodactylus.util.service.AbstractService;
/** The Sone to insert. */
private final Sone sone;
+ /** The insert listener manager. */
+ private SoneInsertListenerManager soneInsertListenerManager;
+
/** Whether a modification has been detected. */
private volatile boolean modified = false;
this.core = core;
this.freenetInterface = freenetInterface;
this.sone = sone;
+ this.soneInsertListenerManager = new SoneInsertListenerManager(sone);
+ }
+
+ //
+ // LISTENER MANAGEMENT
+ //
+
+ /**
+ * Adds a listener for Sone insert events.
+ *
+ * @param soneInsertListener
+ * The Sone insert listener
+ */
+ public void addSoneInsertListener(SoneInsertListener soneInsertListener) {
+ soneInsertListenerManager.addListener(soneInsertListener);
+ }
+
+ /**
+ * Removes a listener for Sone insert events.
+ *
+ * @param soneInsertListener
+ * The Sone insert listener
+ */
+ public void removeSoneInsertListener(SoneInsertListener soneInsertListener) {
+ soneInsertListenerManager.removeListener(soneInsertListener);
}
//
core.setSoneStatus(sone, SoneStatus.inserting);
long insertTime = System.currentTimeMillis();
insertInformation.setTime(insertTime);
+ soneInsertListenerManager.fireInsertStarted();
FreenetURI finalUri = freenetInterface.insertDirectory(insertInformation.getInsertUri(), insertInformation.generateManifestEntries(), "index.html");
+ soneInsertListenerManager.fireInsertFinished(System.currentTimeMillis() - insertTime);
/* at this point we might already be stopped. */
if (shouldStop()) {
/* if so, bail out, don’t change anything. */
}
sone.setTime(insertTime);
sone.setLatestEdition(finalUri.getEdition());
- core.saveSone(sone);
+ core.touchConfiguration();
success = true;
logger.log(Level.INFO, "Inserted Sone “%s” at %s.", new Object[] { sone.getName(), finalUri });
} catch (SoneException se1) {
+ soneInsertListenerManager.fireInsertAborted(se1);
logger.log(Level.WARNING, "Could not insert Sone “" + sone.getName() + "”!", se1);
} finally {
core.setSoneStatus(sone, SoneStatus.idle);
synchronized (sone) {
if (lastInsertFingerprint.equals(sone.getFingerprint())) {
logger.log(Level.FINE, "Sone “%s” was not modified further, resetting counter…", new Object[] { sone });
- core.saveSone(sone);
lastModificationTime = 0;
modified = false;
}
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
- private static class InsertInformation {
+ private class InsertInformation {
/** All properties of the Sone, copied for thread safety. */
private final Map<String, Object> soneProperties = new HashMap<String, Object>();
soneProperties.put("requestUri", sone.getRequestUri());
soneProperties.put("insertUri", sone.getInsertUri());
soneProperties.put("profile", sone.getProfile());
- soneProperties.put("posts", new ArrayList<Post>(sone.getPosts()));
- soneProperties.put("replies", new HashSet<Reply>(sone.getReplies()));
+ soneProperties.put("posts", new ListBuilder<Post>(new ArrayList<Post>(sone.getPosts())).sort(Post.TIME_COMPARATOR).get());
+ soneProperties.put("replies", new ListBuilder<Reply>(new ArrayList<Reply>(sone.getReplies())).sort(new ReverseComparator<Reply>(Reply.TIME_COMPARATOR)).get());
soneProperties.put("likedPostIds", new HashSet<String>(sone.getLikedPostIds()));
soneProperties.put("likedReplyIds", new HashSet<String>(sone.getLikedReplyIds()));
soneProperties.put("albums", Sone.flattenAlbums(sone.getAlbums()));
}
TemplateContext templateContext = templateContextFactory.createTemplateContext();
+ templateContext.set("core", core);
templateContext.set("currentSone", soneProperties);
+ templateContext.set("currentEdition", core.getUpdateChecker().getLatestEdition());
templateContext.set("version", SonePlugin.VERSION);
StringWriter writer = new StringWriter();
StringBucket bucket = null;
--- /dev/null
+/*
+ * Sone - SoneProvider.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core;
+
+import net.pterodactylus.sone.data.Sone;
+
+/**
+ * Interface for objects that can provide {@link Sone}s by their ID.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface SoneProvider {
+
+ /**
+ * Returns the Sone with the given ID, if it exists. If it does not exist
+ * and {@code create} is {@code false}, {@code null} is returned; otherwise,
+ * a new Sone with the given ID is created and returned.
+ *
+ * @param soneId
+ * The ID of the Sone to return
+ * @param create
+ * {@code true} to create a new Sone if no Sone with the given ID
+ * exists, {@code false} to return {@code null} instead
+ * @return The Sone with the given ID, or {@code null}
+ */
+ public Sone getSone(String soneId, boolean create);
+
+}
--- /dev/null
+/*
+ * Sone - SoneRescuer.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.service.AbstractService;
+import freenet.keys.FreenetURI;
+
+/**
+ * The Sone rescuer downloads older editions of a Sone and updates the currently
+ * stored Sone with it.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneRescuer extends AbstractService {
+
+ /** The core. */
+ private final Core core;
+
+ /** The Sone downloader. */
+ private final SoneDownloader soneDownloader;
+
+ /** The Sone being rescued. */
+ private final Sone sone;
+
+ /** Whether the rescuer is currently fetching a Sone. */
+ private volatile boolean fetching;
+
+ /** The currently tried edition. */
+ private volatile long currentEdition;
+
+ /** Whether the last fetch was successful. */
+ private volatile boolean lastFetchSuccessful = true;
+
+ /**
+ * Creates a new Sone rescuer.
+ *
+ * @param core
+ * The core
+ * @param soneDownloader
+ * The Sone downloader
+ * @param sone
+ * The Sone to rescue
+ */
+ public SoneRescuer(Core core, SoneDownloader soneDownloader, Sone sone) {
+ super("Sone Rescuer for " + sone.getName());
+ this.core = core;
+ this.soneDownloader = soneDownloader;
+ this.sone = sone;
+ currentEdition = sone.getRequestUri().getEdition();
+ }
+
+ //
+ // ACCESSORS
+ //
+
+ /**
+ * Returns whether the Sone rescuer is currently fetching a Sone.
+ *
+ * @return {@code true} if the Sone rescuer is currently fetching a Sone
+ */
+ public boolean isFetching() {
+ return fetching;
+ }
+
+ /**
+ * Returns the edition that is currently being downloaded.
+ *
+ * @return The edition that is currently being downloaded
+ */
+ public long getCurrentEdition() {
+ return currentEdition;
+ }
+
+ /**
+ * Returns whether the Sone rescuer can download a next edition.
+ *
+ * @return {@code true} if the Sone rescuer can download a next edition,
+ * {@code false} if the last edition was already tried
+ */
+ public boolean hasNextEdition() {
+ return currentEdition > 0;
+ }
+
+ /**
+ * Returns the next edition the Sone rescuer can download.
+ *
+ * @return The next edition the Sone rescuer can download
+ */
+ public long getNextEdition() {
+ return currentEdition - 1;
+ }
+
+ /**
+ * Sets the edition to rescue.
+ *
+ * @param edition
+ * The edition to rescue
+ * @return This Sone rescuer
+ */
+ public SoneRescuer setEdition(long edition) {
+ currentEdition = edition;
+ return this;
+ }
+
+ /**
+ * Sets whether the last fetch was successful.
+ *
+ * @return {@code true} if the last fetch was successful, {@code false}
+ * otherwise
+ */
+ public boolean isLastFetchSuccessful() {
+ return lastFetchSuccessful;
+ }
+
+ //
+ // ACTIONS
+ //
+
+ /**
+ * Starts the next fetch. If you want to fetch a different edition than “the
+ * next older one,” remember to call {@link #setEdition(long)} before
+ * calling this method.
+ */
+ public void startNextFetch() {
+ fetching = true;
+ notifySyncObject();
+ }
+
+ //
+ // SERVICE METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void serviceRun() {
+ while (!shouldStop()) {
+ while (!shouldStop() && !fetching) {
+ sleep();
+ }
+ if (fetching) {
+ core.lockSone(sone);
+ FreenetURI soneUri = sone.getRequestUri().setKeyType("SSK").setDocName("Sone-" + currentEdition).setMetaString(new String[] { "sone.xml" });
+ System.out.println("URI: " + soneUri);
+ Sone fetchedSone = soneDownloader.fetchSone(sone, soneUri, true);
+ System.out.println("Sone: " + fetchedSone);
+ lastFetchSuccessful = (fetchedSone != null);
+ if (lastFetchSuccessful) {
+ core.updateSone(fetchedSone, true);
+ }
+ fetching = false;
+ }
+ }
+ }
+
+}
private static final String SONE_HOMEPAGE = "USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/";
/** The current latest known edition. */
- private static final int LATEST_EDITION = 25;
+ private static final int LATEST_EDITION = 36;
/** The Freenet interface. */
private final FreenetInterface freenetInterface;
/*
- * FreenetSone - StatusUpdate.java - Copyright © 2010 David Roden
+ * Sone - Post.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
/*
- * FreenetSone - Profile.java - Copyright © 2010 David Roden
+ * Sone - Profile.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
/*
- * FreenetSone - Sone.java - Copyright © 2010 David Roden
+ * Sone - Sone.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
import java.util.logging.Level;
import java.util.logging.Logger;
+import net.pterodactylus.sone.core.Core;
import net.pterodactylus.sone.core.Options;
import net.pterodactylus.sone.freenet.wot.Identity;
+import net.pterodactylus.sone.freenet.wot.OwnIdentity;
import net.pterodactylus.sone.template.SoneAccessor;
import net.pterodactylus.util.filter.Filter;
import net.pterodactylus.util.logging.Logging;
};
+ /**
+ * Comparator that sorts Sones by last activity (least recent active first).
+ */
+ public static final Comparator<Sone> LAST_ACTIVITY_COMPARATOR = new Comparator<Sone>() {
+
+ @Override
+ public int compare(Sone firstSone, Sone secondSone) {
+ return (int) Math.min(Integer.MAX_VALUE, Math.max(Integer.MIN_VALUE, secondSone.getTime() - firstSone.getTime()));
+ }
+ };
+
+ /** Comparator that sorts Sones by numbers of posts (descending). */
+ public static final Comparator<Sone> POST_COUNT_COMPARATOR = new Comparator<Sone>() {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int compare(Sone leftSone, Sone rightSone) {
+ return (leftSone.getPosts().size() != rightSone.getPosts().size()) ? (rightSone.getPosts().size() - leftSone.getPosts().size()) : (rightSone.getReplies().size() - leftSone.getReplies().size());
+ }
+ };
+
/** Filter to remove Sones that have not been downloaded. */
public static final Filter<Sone> EMPTY_SONE_FILTER = new Filter<Sone>() {
}
};
+ /** Filter that matches all {@link Core#isLocalSone(Sone) local Sones}. */
+ public static final Filter<Sone> LOCAL_SONE_FILTER = new Filter<Sone>() {
+
+ @Override
+ public boolean filterObject(Sone sone) {
+ return sone.getIdentity() instanceof OwnIdentity;
+ }
+
+ };
+
/** The logger. */
private static final Logger logger = Logging.getLogger(Sone.class);
}
fingerprint.append(")");
+ @SuppressWarnings("hiding")
List<Reply> replies = new ArrayList<Reply>(getReplies());
Collections.sort(replies, Reply.TIME_COMPARATOR);
fingerprint.append("Replies(");
}
fingerprint.append(')');
+ @SuppressWarnings("hiding")
List<String> likedPostIds = new ArrayList<String>(getLikedPostIds());
Collections.sort(likedPostIds);
fingerprint.append("LikedPosts(");
}
fingerprint.append(')');
+ @SuppressWarnings("hiding")
List<String> likedReplyIds = new ArrayList<String>(getLikedReplyIds());
Collections.sort(likedReplyIds);
fingerprint.append("LikedReplies(");
--- /dev/null
+/*
+ * Sone - FcpInterface.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.fcp;
+
+import java.util.Collection;
+import java.util.List;
+
+import net.pterodactylus.sone.core.Core;
+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.data.Profile.Field;
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import net.pterodactylus.sone.freenet.fcp.AbstractCommand;
+import net.pterodactylus.sone.freenet.fcp.Command;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import net.pterodactylus.sone.template.SoneAccessor;
+import net.pterodactylus.util.filter.Filters;
+import freenet.node.FSParseException;
+import freenet.support.SimpleFieldSet;
+
+/**
+ * Abstract base implementation of a {@link Command} with Sone-related helper
+ * methods.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class AbstractSoneCommand extends AbstractCommand {
+
+ /** The Sone core. */
+ private final Core core;
+
+ /** Whether this command needs write access. */
+ private final boolean writeAccess;
+
+ /**
+ * Creates a new abstract Sone FCP command.
+ *
+ * @param core
+ * The Sone core
+ */
+ protected AbstractSoneCommand(Core core) {
+ this(core, false);
+ }
+
+ /**
+ * Creates a new abstract Sone FCP command.
+ *
+ * @param core
+ * The Sone core
+ * @param writeAccess
+ * {@code true} if this command requires write access,
+ * {@code false} otherwise
+ */
+ protected AbstractSoneCommand(Core core, boolean writeAccess) {
+ this.core = core;
+ this.writeAccess = writeAccess;
+ }
+
+ //
+ // ACCESSORS
+ //
+
+ /**
+ * Returns the Sone core.
+ *
+ * @return The Sone core
+ */
+ protected Core getCore() {
+ return core;
+ }
+
+ /**
+ * Returns whether this command requires write access.
+ *
+ * @return {@code true} if this command require write access, {@code false}
+ * otherwise
+ */
+ public boolean requiresWriteAccess() {
+ return writeAccess;
+ }
+
+ //
+ // PROTECTED METHODS
+ //
+
+ /**
+ * Encodes text in a way that makes it possible for the text to be stored in
+ * a {@link SimpleFieldSet}. Backslashes, CR, and LF are prepended with a
+ * backslash.
+ *
+ * @param text
+ * The text to encode
+ * @return The encoded text
+ */
+ protected String encodeString(String text) {
+ return text.replaceAll("\\\\", "\\\\").replaceAll("\n", "\\\\n").replaceAll("\r", "\\\\r");
+ }
+
+ /**
+ * Returns a Sone whose ID is a parameter in the given simple field set.
+ *
+ * @param simpleFieldSet
+ * The simple field set containing the ID of the Sone
+ * @param parameterName
+ * The name under which the Sone ID is stored in the simple field
+ * set
+ * @param localOnly
+ * {@code true} to only return local Sones, {@code false} to
+ * return any Sones
+ * @return The Sone
+ * @throws FcpException
+ * if there is no Sone ID stored under the given parameter name,
+ * or if the Sone ID is invalid
+ */
+ protected Sone getSone(SimpleFieldSet simpleFieldSet, String parameterName, boolean localOnly) throws FcpException {
+ return getSone(simpleFieldSet, parameterName, localOnly, true);
+ }
+
+ /**
+ * Returns a Sone whose ID is a parameter in the given simple field set.
+ *
+ * @param simpleFieldSet
+ * The simple field set containing the ID of the Sone
+ * @param parameterName
+ * The name under which the Sone ID is stored in the simple field
+ * set
+ * @param localOnly
+ * {@code true} to only return local Sones, {@code false} to
+ * return any Sones
+ * @param mandatory
+ * {@code true} if a valid Sone ID is required, {@code false}
+ * otherwise
+ * @return The Sone, or {@code null} if {@code mandatory} is {@code false}
+ * and the Sone ID is invalid
+ * @throws FcpException
+ * if there is no Sone ID stored under the given parameter name,
+ * or if {@code mandatory} is {@code true} and the Sone ID is
+ * invalid
+ */
+ protected Sone getSone(SimpleFieldSet simpleFieldSet, String parameterName, boolean localOnly, boolean mandatory) throws FcpException {
+ String soneId = simpleFieldSet.get(parameterName);
+ if (mandatory && (soneId == null)) {
+ throw new FcpException("Could not load Sone ID from “" + parameterName + "”.");
+ }
+ Sone sone = localOnly ? core.getLocalSone(soneId, false) : core.getSone(soneId, false);
+ if (mandatory && (sone == null)) {
+ throw new FcpException("Could not load Sone from “" + soneId + "”.");
+ }
+ return sone;
+ }
+
+ /**
+ * Returns a post whose ID is a parameter in the given simple field set.
+ *
+ * @param simpleFieldSet
+ * The simple field set containing the ID of the post
+ * @param parameterName
+ * The name under which the post ID is stored in the simple field
+ * set
+ * @return The post
+ * @throws FcpException
+ * if there is no post ID stored under the given parameter name,
+ * or if the post ID is invalid
+ */
+ protected Post getPost(SimpleFieldSet simpleFieldSet, String parameterName) throws FcpException {
+ try {
+ String postId = simpleFieldSet.getString(parameterName);
+ Post post = core.getPost(postId, false);
+ if (post == null) {
+ throw new FcpException("Could not load post from “" + postId + "”.");
+ }
+ return post;
+ } catch (FSParseException fspe1) {
+ throw new FcpException("Could not post ID from “" + parameterName + "”.", fspe1);
+ }
+ }
+
+ /**
+ * Returns a reply whose ID is a parameter in the given simple field set.
+ *
+ * @param simpleFieldSet
+ * The simple field set containing the ID of the reply
+ * @param parameterName
+ * The name under which the reply ID is stored in the simple
+ * field set
+ * @return The reply
+ * @throws FcpException
+ * if there is no reply ID stored under the given parameter
+ * name, or if the reply ID is invalid
+ */
+ protected Reply getReply(SimpleFieldSet simpleFieldSet, String parameterName) throws FcpException {
+ try {
+ String replyId = simpleFieldSet.getString(parameterName);
+ Reply reply = core.getReply(replyId, false);
+ if (reply == null) {
+ throw new FcpException("Could not load reply from “" + replyId + "”.");
+ }
+ return reply;
+ } catch (FSParseException fspe1) {
+ throw new FcpException("Could not reply ID from “" + parameterName + "”.", fspe1);
+ }
+ }
+
+ /**
+ * Creates a simple field set from the given Sone, including {@link Profile}
+ * information.
+ *
+ * @param sone
+ * The Sone to encode
+ * @param prefix
+ * The prefix for the field names (may be empty but not {@code
+ * null})
+ * @param localSone
+ * An optional local Sone that is used for Sone-specific data,
+ * such as if the Sone is followed by the local Sone
+ * @return The simple field set containing the given Sone
+ */
+ protected SimpleFieldSet encodeSone(Sone sone, String prefix, Sone localSone) {
+ SimpleFieldSetBuilder soneBuilder = new SimpleFieldSetBuilder();
+
+ soneBuilder.put(prefix + "Name", sone.getName());
+ soneBuilder.put(prefix + "NiceName", SoneAccessor.getNiceName(sone));
+ soneBuilder.put(prefix + "LastUpdated", sone.getTime());
+ if (localSone != null) {
+ soneBuilder.put(prefix + "Followed", String.valueOf(localSone.hasFriend(sone.getId())));
+ }
+ Profile profile = sone.getProfile();
+ soneBuilder.put(prefix + "Field.Count", profile.getFields().size());
+ int fieldIndex = 0;
+ for (Field field : profile.getFields()) {
+ soneBuilder.put(prefix + "Field." + fieldIndex + ".Name", field.getName());
+ soneBuilder.put(prefix + "Field." + fieldIndex + ".Value", field.getValue());
+ ++fieldIndex;
+ }
+
+ return soneBuilder.get();
+ }
+
+ /**
+ * Creates a simple field set from the given collection of Sones.
+ *
+ * @param sones
+ * The Sones to encode
+ * @param prefix
+ * The prefix for the field names (may be empty but not
+ * {@code null})
+ * @return The simple field set containing the given Sones
+ */
+ protected SimpleFieldSet encodeSones(Collection<? extends Sone> sones, String prefix) {
+ SimpleFieldSetBuilder soneBuilder = new SimpleFieldSetBuilder();
+
+ int soneIndex = 0;
+ soneBuilder.put(prefix + "Count", sones.size());
+ for (Sone sone : sones) {
+ String sonePrefix = prefix + soneIndex++ + ".";
+ soneBuilder.put(sonePrefix + "ID", sone.getId());
+ soneBuilder.put(sonePrefix + "Name", sone.getName());
+ soneBuilder.put(sonePrefix + "NiceName", SoneAccessor.getNiceName(sone));
+ soneBuilder.put(sonePrefix + "Time", sone.getTime());
+ }
+
+ return soneBuilder.get();
+ }
+
+ /**
+ * Creates a simple field set from the given post.
+ *
+ * @param post
+ * The post to encode
+ * @param prefix
+ * The prefix for the field names (may be empty but not
+ * {@code null})
+ * @param includeReplies
+ * {@code true} to include replies, {@code false} to not include
+ * replies
+ * @return The simple field set containing the post
+ */
+ protected SimpleFieldSet encodePost(Post post, String prefix, boolean includeReplies) {
+ SimpleFieldSetBuilder postBuilder = new SimpleFieldSetBuilder();
+
+ postBuilder.put(prefix + "ID", post.getId());
+ postBuilder.put(prefix + "Sone", post.getSone().getId());
+ if (post.getRecipient() != null) {
+ postBuilder.put(prefix + "Recipient", post.getRecipient().getId());
+ }
+ postBuilder.put(prefix + "Time", post.getTime());
+ postBuilder.put(prefix + "Text", encodeString(post.getText()));
+ postBuilder.put(encodeLikes(core.getLikes(post), prefix + "Likes."));
+
+ if (includeReplies) {
+ List<Reply> replies = core.getReplies(post);
+ postBuilder.put(encodeReplies(replies, prefix));
+ }
+
+ return postBuilder.get();
+ }
+
+ /**
+ * Creates a simple field set from the given collection of posts.
+ *
+ * @param posts
+ * The posts to encode
+ * @param prefix
+ * The prefix for the field names (may be empty but not
+ * {@code null})
+ * @param includeReplies
+ * {@code true} to include the replies, {@code false} to not
+ * include the replies
+ * @return The simple field set containing the posts
+ */
+ protected SimpleFieldSet encodePosts(Collection<? extends Post> posts, String prefix, boolean includeReplies) {
+ SimpleFieldSetBuilder postBuilder = new SimpleFieldSetBuilder();
+
+ int postIndex = 0;
+ postBuilder.put(prefix + "Count", posts.size());
+ for (Post post : posts) {
+ String postPrefix = prefix + postIndex++;
+ postBuilder.put(encodePost(post, postPrefix + ".", includeReplies));
+ if (includeReplies) {
+ postBuilder.put(encodeReplies(Filters.filteredList(core.getReplies(post), Reply.FUTURE_REPLIES_FILTER), postPrefix + "."));
+ }
+ }
+
+ return postBuilder.get();
+ }
+
+ /**
+ * Creates a simple field set from the given collection of replies.
+ *
+ * @param replies
+ * The replies to encode
+ * @param prefix
+ * The prefix for the field names (may be empty, but not
+ * {@code null})
+ * @return The simple field set containing the replies
+ */
+ protected SimpleFieldSet encodeReplies(Collection<? extends Reply> replies, String prefix) {
+ SimpleFieldSetBuilder replyBuilder = new SimpleFieldSetBuilder();
+
+ int replyIndex = 0;
+ replyBuilder.put(prefix + "Replies.Count", replies.size());
+ for (Reply reply : replies) {
+ String replyPrefix = prefix + "Replies." + replyIndex++ + ".";
+ replyBuilder.put(replyPrefix + "ID", reply.getId());
+ replyBuilder.put(replyPrefix + "Sone", reply.getSone().getId());
+ replyBuilder.put(replyPrefix + "Time", reply.getTime());
+ replyBuilder.put(replyPrefix + "Text", encodeString(reply.getText()));
+ }
+
+ return replyBuilder.get();
+ }
+
+ /**
+ * Creates a simple field set from the given collection of Sones that like
+ * an element.
+ *
+ * @param likes
+ * The liking Sones
+ * @param prefix
+ * The prefix for the field names (may be empty but not
+ * {@code null})
+ * @return The simple field set containing the likes
+ */
+ protected SimpleFieldSet encodeLikes(Collection<? extends Sone> likes, String prefix) {
+ SimpleFieldSetBuilder likesBuilder = new SimpleFieldSetBuilder();
+
+ int likeIndex = 0;
+ likesBuilder.put(prefix + "Count", likes.size());
+ for (Sone sone : likes) {
+ String sonePrefix = prefix + likeIndex++ + ".";
+ likesBuilder.put(sonePrefix + "ID", sone.getId());
+ }
+
+ return likesBuilder.get();
+ }
+
+ //
+ // OBJECT METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return getClass().getName() + "[writeAccess=" + writeAccess + "]";
+ }
+
+}
--- /dev/null
+/*
+ * Sone - CreatePostCommand.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * FCP command that creates a new {@link Post}.
+ *
+ * @see Core#createPost(Sone, Sone, String)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class CreatePostCommand extends AbstractSoneCommand {
+
+ /**
+ * Creates a new “CreatePost” FCP command.
+ *
+ * @param core
+ * The Sone core
+ */
+ public CreatePostCommand(Core core) {
+ super(core, true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+ Sone sone = getSone(parameters, "Sone", true);
+ String text = getString(parameters, "Text");
+ Sone recipient = null;
+ if (parameters.get("Recipient") != null) {
+ recipient = getSone(parameters, "Recipient", false);
+ }
+ if (sone.equals(recipient)) {
+ return new ErrorResponse("Sone and Recipient must not be the same.");
+ }
+ Post post = getCore().createPost(sone, recipient, text);
+ return new Response("PostCreated", new SimpleFieldSetBuilder().put("Post", post.getId()).get());
+ }
+
+}
--- /dev/null
+/*
+ * Sone - CreateReplyCommand.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * FCP command that creates a new {@link Reply}.
+ *
+ * @see Core#createReply(Sone, Post, String)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class CreateReplyCommand extends AbstractSoneCommand {
+
+ /**
+ * Creates a new “CreateReply” FCP command.
+ *
+ * @param core
+ * The Sone core
+ */
+ public CreateReplyCommand(Core core) {
+ super(core, true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+ Sone sone = getSone(parameters, "Sone", true);
+ Post post = getPost(parameters, "Post");
+ String text = getString(parameters, "Text");
+ Reply reply = getCore().createReply(sone, post, text);
+ return new Response("ReplyCreated", new SimpleFieldSetBuilder().put("Reply", reply.getId()).get());
+ }
+
+}
--- /dev/null
+/*
+ * Sone - DeletePostCommand.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * FCP command that deletes a {@link Post}.
+ *
+ * @see Core#deletePost(Post)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DeletePostCommand extends AbstractSoneCommand {
+
+ /**
+ * Creates a new “DeletePost” FCP command.
+ *
+ * @param core
+ * The Sone core
+ */
+ public DeletePostCommand(Core core) {
+ super(core, true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+ Post post = getPost(parameters, "Post");
+ if (!getCore().isLocalSone(post.getSone())) {
+ return new ErrorResponse(401, "Not allowed.");
+ }
+ return new Response("PostDeleted", new SimpleFieldSetBuilder().get());
+ }
+
+}
--- /dev/null
+/*
+ * Sone - DeleteReplyCommand.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * FCP command that deletes a {@link Reply}.
+ *
+ * @see Core#deleteReply(Reply)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DeleteReplyCommand extends AbstractSoneCommand {
+
+ /**
+ * Creates a new “DeleteReply” FCP command.
+ *
+ * @param core
+ * The Sone core
+ */
+ public DeleteReplyCommand(Core core) {
+ super(core, true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+ Reply reply = getReply(parameters, "Reply");
+ if (!getCore().isLocalSone(reply.getSone())) {
+ return new ErrorResponse(401, "Not allowed.");
+ }
+ return new Response("ReplyDeleted", new SimpleFieldSetBuilder().get());
+ }
+
+}
--- /dev/null
+/*
+ * Sone - FcpInterface.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.fcp;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.freenet.fcp.Command.AccessType;
+import net.pterodactylus.sone.freenet.fcp.Command.ErrorResponse;
+import net.pterodactylus.sone.freenet.fcp.Command.Response;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.validation.Validation;
+import freenet.pluginmanager.FredPluginFCP;
+import freenet.pluginmanager.PluginNotFoundException;
+import freenet.pluginmanager.PluginReplySender;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implementation of an FCP interface for other clients or plugins to
+ * communicate with Sone.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FcpInterface {
+
+ /**
+ * The action level that full access for the FCP connection is required.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ public enum FullAccessRequired {
+
+ /** No action requires full access. */
+ NO,
+
+ /** All writing actions require full access. */
+ WRITING,
+
+ /** All actions require full access. */
+ ALWAYS,
+
+ }
+
+ /** The logger. */
+ private static final Logger logger = Logging.getLogger(FcpInterface.class);
+
+ /** Whether the FCP interface is currently active. */
+ private volatile boolean active;
+
+ /** What function full access is required for. */
+ private volatile FullAccessRequired fullAccessRequired = FullAccessRequired.ALWAYS;
+
+ /** All available FCP commands. */
+ private final Map<String, AbstractSoneCommand> commands = Collections.synchronizedMap(new HashMap<String, AbstractSoneCommand>());
+
+ /**
+ * Creates a new FCP interface.
+ *
+ * @param core
+ * The core
+ */
+ public FcpInterface(Core core) {
+ commands.put("Version", new VersionCommand(core));
+ commands.put("GetLocalSones", new GetLocalSonesCommand(core));
+ commands.put("GetSones", new GetSonesCommand(core));
+ commands.put("GetSone", new GetSoneCommand(core));
+ commands.put("GetPost", new GetPostCommand(core));
+ commands.put("GetPosts", new GetPostsCommand(core));
+ commands.put("GetPostFeed", new GetPostFeedCommand(core));
+ commands.put("LikePost", new LikePostCommand(core));
+ commands.put("LikeReply", new LikeReplyCommand(core));
+ commands.put("CreatePost", new CreatePostCommand(core));
+ commands.put("CreateReply", new CreateReplyCommand(core));
+ commands.put("DeletePost", new DeletePostCommand(core));
+ commands.put("DeleteReply", new DeleteReplyCommand(core));
+ }
+
+ //
+ // ACCESSORS
+ //
+
+ /**
+ * Sets whether the FCP interface should handle requests. If {@code active}
+ * is {@code false}, all requests are answered with an error.
+ *
+ * @param active
+ * {@code true} to activate the FCP interface, {@code false} to
+ * deactivate the FCP interface
+ */
+ public void setActive(boolean active) {
+ this.active = active;
+ }
+
+ /**
+ * Sets the action level for which full FCP access is required.
+ *
+ * @param fullAccessRequired
+ * The action level for which full FCP access is required
+ */
+ public void setFullAccessRequired(FullAccessRequired fullAccessRequired) {
+ Validation.begin().isNotNull("FullAccessRequired", fullAccessRequired).check();
+ this.fullAccessRequired = fullAccessRequired;
+ }
+
+ //
+ // ACTIONS
+ //
+
+ /**
+ * Handles a plugin FCP request.
+ *
+ * @param pluginReplySender
+ * The reply sender
+ * @param parameters
+ * The message parameters
+ * @param data
+ * The message data (may be {@code null})
+ * @param accessType
+ * One of {@link FredPluginFCP#ACCESS_DIRECT},
+ * {@link FredPluginFCP#ACCESS_FCP_FULL},
+ * {@link FredPluginFCP#ACCESS_FCP_RESTRICTED}
+ */
+ public void handle(PluginReplySender pluginReplySender, SimpleFieldSet parameters, Bucket data, int accessType) {
+ if (!active) {
+ try {
+ sendReply(pluginReplySender, null, new ErrorResponse(400, "FCP Interface deactivated"));
+ } catch (PluginNotFoundException pnfe1) {
+ logger.log(Level.FINE, "Could not set error to plugin.", pnfe1);
+ }
+ return;
+ }
+ AbstractSoneCommand command = commands.get(parameters.get("Message"));
+ if ((accessType == FredPluginFCP.ACCESS_FCP_RESTRICTED) && (((fullAccessRequired == FullAccessRequired.WRITING) && command.requiresWriteAccess()) || (fullAccessRequired == FullAccessRequired.ALWAYS))) {
+ try {
+ sendReply(pluginReplySender, null, new ErrorResponse(401, "Not authorized"));
+ } catch (PluginNotFoundException pnfe1) {
+ logger.log(Level.FINE, "Could not set error to plugin.", pnfe1);
+ }
+ return;
+ }
+ try {
+ if (command == null) {
+ sendReply(pluginReplySender, null, new ErrorResponse("Unrecognized Message: " + parameters.get("Message")));
+ return;
+ }
+ String identifier = parameters.get("Identifier");
+ if ((identifier == null) || (identifier.length() == 0)) {
+ sendReply(pluginReplySender, null, new ErrorResponse("Missing Identifier."));
+ return;
+ }
+ try {
+ Response response = command.execute(parameters, data, AccessType.values()[accessType]);
+ sendReply(pluginReplySender, identifier, response);
+ } catch (FcpException fe1) {
+ logger.log(Level.WARNING, "Could not process FCP command “%s”.", command);
+ sendReply(pluginReplySender, identifier, new ErrorResponse("Error executing command: " + fe1.getMessage()));
+ }
+ } catch (PluginNotFoundException pnfe1) {
+ logger.log(Level.WARNING, "Could not find destination plugin: " + pluginReplySender);
+ }
+ }
+
+ //
+ // PRIVATE METHODS
+ //
+
+ /**
+ * Sends the given response to the given plugin.
+ *
+ * @param pluginReplySender
+ * The reply sender
+ * @param identifier
+ * The identifier (may be {@code null})
+ * @param response
+ * The response to send
+ * @throws PluginNotFoundException
+ * if the plugin can not be found
+ */
+ private void sendReply(PluginReplySender pluginReplySender, String identifier, Response response) throws PluginNotFoundException {
+ SimpleFieldSet replyParameters = response.getReplyParameters();
+ if (identifier != null) {
+ replyParameters.putOverwrite("Identifier", identifier);
+ }
+ if (response.hasData()) {
+ pluginReplySender.send(replyParameters, response.getData());
+ } else if (response.hasBucket()) {
+ pluginReplySender.send(replyParameters, response.getBucket());
+ } else {
+ pluginReplySender.send(replyParameters);
+ }
+ }
+
+}
--- /dev/null
+/*
+ * Sone - GetLocalSonesCommand.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implements the “GetLocalSones” FCP command that returns the list of local
+ * Sones to the sender.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetLocalSonesCommand extends AbstractSoneCommand {
+
+ /**
+ * Creates a new “GetLocalSones” FCP command.
+ *
+ * @param core
+ * The Sone core
+ */
+ public GetLocalSonesCommand(Core core) {
+ super(core);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+ return new Response("ListLocalSones", encodeSones(getCore().getLocalSones(), "LocalSones."));
+ }
+
+}
--- /dev/null
+/*
+ * Sone - GetPostCommand.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * The “GetPost” FCP command returns a single {@link Post} to an FCP client.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetPostCommand extends AbstractSoneCommand {
+
+ /**
+ * Creates a new “GetPost” FCP command.
+ *
+ * @param core
+ * The Sone core
+ */
+ public GetPostCommand(Core core) {
+ super(core);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+ Post post = getPost(parameters, "Post");
+ boolean includeReplies = getBoolean(parameters, "IncludeReplies", true);
+
+ return new Response("Post", encodePost(post, "Post.", includeReplies));
+ }
+
+}
--- /dev/null
+/*
+ * Sone - GetPostFeedCommand.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.fcp;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import net.pterodactylus.util.filter.Filters;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implementation of an FCP interface for other clients or plugins to
+ * communicate with Sone.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetPostFeedCommand extends AbstractSoneCommand {
+
+ /**
+ * Creates a new “GetPostFeed” command.
+ *
+ * @param core
+ * The core
+ */
+ public GetPostFeedCommand(Core core) {
+ super(core);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+ Sone sone = getSone(parameters, "Sone", true);
+ int startPost = getInt(parameters, "StartPost", 0);
+ int maxPosts = getInt(parameters, "MaxPosts", -1);
+
+ Set<Post> allPosts = new HashSet<Post>();
+ allPosts.addAll(sone.getPosts());
+ for (String friendSoneId : sone.getFriends()) {
+ if (!getCore().hasSone(friendSoneId)) {
+ continue;
+ }
+ allPosts.addAll(getCore().getSone(friendSoneId).getPosts());
+ }
+ allPosts.addAll(getCore().getDirectedPosts(sone));
+ allPosts = Filters.filteredSet(allPosts, Post.FUTURE_POSTS_FILTER);
+
+ List<Post> sortedPosts = new ArrayList<Post>(allPosts);
+ Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
+
+ if (sortedPosts.size() < startPost) {
+ return new Response("PostFeed", encodePosts(Collections.<Post> emptyList(), "Posts.", false));
+ }
+
+ return new Response("PostFeed", encodePosts(sortedPosts.subList(startPost, (maxPosts == -1) ? sortedPosts.size() : Math.min(startPost + maxPosts, sortedPosts.size())), "Posts.", true));
+ }
+
+}
--- /dev/null
+/*
+ * Sone - GetPostsCommand.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.fcp;
+
+import java.util.Collections;
+import java.util.List;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implements the “GetPosts” FCP command that returns the list of posts a Sone
+ * made.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetPostsCommand extends AbstractSoneCommand {
+
+ /**
+ * Creates a new “GetPosts” FCP command.
+ *
+ * @param core
+ * The Sone core
+ */
+ public GetPostsCommand(Core core) {
+ super(core);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+ Sone sone = getSone(parameters, "Sone", false);
+ int startPost = getInt(parameters, "StartPost", 0);
+ int maxPosts = getInt(parameters, "MaxPosts", -1);
+ List<Post> posts = sone.getPosts();
+ if (posts.size() < startPost) {
+ return new Response("Posts", encodePosts(Collections.<Post> emptyList(), "Posts.", false));
+ }
+ return new Response("Posts", encodePosts(sone.getPosts().subList(startPost, (maxPosts == -1) ? posts.size() : Math.min(startPost + maxPosts, posts.size())), "Posts.", true));
+ }
+
+}
--- /dev/null
+/*
+ * Sone - GetSoneCommand.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implements the “GetSone“ FCP command which returns {@link Profile}
+ * information about a {@link Sone}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetSoneCommand extends AbstractSoneCommand {
+
+ /**
+ * Creates a new “GetSone” FCP command.
+ *
+ * @param core
+ * The Sone core
+ */
+ protected GetSoneCommand(Core core) {
+ super(core);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+ Sone sone = getSone(parameters, "Sone", false);
+ Sone localSone = getSone(parameters, "LocalSone", false, false);
+ return new Response("Sone", encodeSone(sone, "", localSone));
+ }
+
+}
--- /dev/null
+/*
+ * Sone - GetSonesCommand.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.fcp;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implements the “GetSones” FCP command that returns the list of known Sones.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetSonesCommand extends AbstractSoneCommand {
+
+ /**
+ * Creates a new “GetSones” FCP command.
+ *
+ * @param core
+ * The Sone core
+ */
+ public GetSonesCommand(Core core) {
+ super(core);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+ int startSone = getInt(parameters, "StartSone", 0);
+ int maxSones = getInt(parameters, "MaxSones", -1);
+ List<Sone> sones = new ArrayList<Sone>(getCore().getSones());
+ if (sones.size() < startSone) {
+ return new Response("Sones", encodeSones(Collections.<Sone> emptyList(), ""));
+ }
+ Collections.sort(sones, Sone.NICE_NAME_COMPARATOR);
+ return new Response("Sones", encodeSones(sones.subList(startSone, (maxSones == -1) ? sones.size() : Math.min(startSone + maxSones, sones.size())), ""));
+ }
+
+}
--- /dev/null
+/*
+ * Sone - LikePostCommand.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implements the “LikePost” FCP command which allows the user to like a post.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class LikePostCommand extends AbstractSoneCommand {
+
+ /**
+ * Creates a new “LikePost” FCP command.
+ *
+ * @param core
+ * The Sone core
+ */
+ protected LikePostCommand(Core core) {
+ super(core, true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+ Post post = getPost(parameters, "Post");
+ Sone sone = getSone(parameters, "Sone", true);
+ sone.addLikedPostId(post.getId());
+ return new Response("PostLiked", new SimpleFieldSetBuilder().put("LikeCount", getCore().getLikes(post).size()).get());
+ }
+
+}
--- /dev/null
+/*
+ * Sone - LikeReplyCommand.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import net.pterodactylus.sone.freenet.fcp.FcpException;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implements the “LikeReply” FCP command which allows the user to like a reply.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class LikeReplyCommand extends AbstractSoneCommand {
+
+ /**
+ * Creates a new “LikeReply” FCP command.
+ *
+ * @param core
+ * The Sone core
+ */
+ protected LikeReplyCommand(Core core) {
+ super(core, true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException {
+ Reply reply = getReply(parameters, "Reply");
+ Sone sone = getSone(parameters, "Sone", true);
+ sone.addLikedReplyId(reply.getId());
+ return new Response("ReplyLiked", new SimpleFieldSetBuilder().put("LikeCount", getCore().getLikes(reply).size()).get());
+ }
+
+}
--- /dev/null
+/*
+ * Sone - VersionCommand.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.fcp;
+
+import net.pterodactylus.sone.core.Core;
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import net.pterodactylus.sone.main.SonePlugin;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Returns version information about the Sone plugin.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class VersionCommand extends AbstractSoneCommand {
+
+ /**
+ * Creates a new “Version” FCP command.
+ *
+ * @param core
+ * The Sone core
+ */
+ protected VersionCommand(Core core) {
+ super(core);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) {
+ return new Response("Version", new SimpleFieldSetBuilder().put("Version", SonePlugin.VERSION.toString()).put("ProtocolVersion", 1).get());
+ }
+
+}
/*
- * FreenetSone - L10nFilter.java - Copyright © 2010 David Roden
+ * Sone - L10nFilter.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
package net.pterodactylus.sone.freenet;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
import java.util.Map;
import net.pterodactylus.util.template.Filter;
*/
@Override
public String format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
- return l10n.getString(String.valueOf(data));
+ if (parameters.isEmpty()) {
+ return l10n.getString(String.valueOf(data));
+ }
+ List<Object> parameterValues = new ArrayList<Object>();
+ int parameterIndex = 0;
+ while (parameters.containsKey(String.valueOf(parameterIndex))) {
+ Object value = parameters.get(String.valueOf(parameterIndex));
+ if (((String) value).startsWith("=")) {
+ value = ((String) value).substring(1);
+ } else {
+ value = templateContext.get((String) value);
+ }
+ parameterValues.add(value);
+ ++parameterIndex;
+ }
+ return new MessageFormat(l10n.getString(String.valueOf(data)), new Locale(l10n.getSelectedLanguage().shortCode)).format(parameterValues.toArray());
}
-
}
/*
- * FreenetSone - PluginStoreConfigurationBackend.java - Copyright © 2010 David Roden
+ * Sone - PluginStoreConfigurationBackend.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
--- /dev/null
+/*
+ * Sone - SimpleFieldSetBuilder.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet;
+
+import net.pterodactylus.util.validation.Validation;
+import freenet.support.SimpleFieldSet;
+
+/**
+ * Helper class to construct {@link SimpleFieldSet} objects in a single call.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SimpleFieldSetBuilder {
+
+ /** The simple field set that is being constructed. */
+ private final SimpleFieldSet simpleFieldSet;
+
+ /**
+ * Creates a new simple field set builder using a new, empty simple field
+ * set.
+ */
+ public SimpleFieldSetBuilder() {
+ this(new SimpleFieldSet(true));
+ }
+
+ /**
+ * Creates a new simple field set builder that will return the given simple
+ * field set on {@link #get()}.
+ *
+ * @param simpleFieldSet
+ * The simple field set to build
+ */
+ public SimpleFieldSetBuilder(SimpleFieldSet simpleFieldSet) {
+ Validation.begin().isNotNull("Simple Field Set", simpleFieldSet).check();
+ this.simpleFieldSet = simpleFieldSet;
+ }
+
+ /**
+ * Returns the constructed simple field set.
+ *
+ * @return The construct simple field set
+ */
+ public SimpleFieldSet get() {
+ return simpleFieldSet;
+ }
+
+ /**
+ * Copies the given simple field set into the simple field set being built
+ * in this builder, overwriting all previously existing values.
+ *
+ * @param simpleFieldSet
+ * The simple field set to copy
+ * @return This simple field set builder
+ */
+ public SimpleFieldSetBuilder put(@SuppressWarnings("hiding") SimpleFieldSet simpleFieldSet) {
+ this.simpleFieldSet.putAllOverwrite(simpleFieldSet);
+ return this;
+ }
+
+ /**
+ * Stores the given value under the given key, overwriting any previous
+ * value.
+ *
+ * @param key
+ * The key of the value
+ * @param value
+ * The value to store
+ * @return This simple field set builder
+ */
+ public SimpleFieldSetBuilder put(String key, String value) {
+ simpleFieldSet.putOverwrite(key, value);
+ return this;
+ }
+
+ /**
+ * Stores the given value under the given key, overwriting any previous
+ * value.
+ *
+ * @param key
+ * The key of the value
+ * @param value
+ * The value to store
+ * @return This simple field set builder
+ */
+ public SimpleFieldSetBuilder put(String key, int value) {
+ simpleFieldSet.put(key, value);
+ return this;
+ }
+
+ /**
+ * Stores the given value under the given key, overwriting any previous
+ * value.
+ *
+ * @param key
+ * The key of the value
+ * @param value
+ * The value to store
+ * @return This simple field set builder
+ */
+ public SimpleFieldSetBuilder put(String key, long value) {
+ simpleFieldSet.put(key, value);
+ return this;
+ }
+
+}
/*
- * FreenetSone - TemplateBucket.java - Copyright © 2010 David Roden
+ * Sone - StringBucket.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
--- /dev/null
+/*
+ * Sone - AbstractCommand.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.fcp;
+
+import freenet.node.FSParseException;
+import freenet.support.SimpleFieldSet;
+
+/**
+ * Basic implementation of a {@link Command} with various helper methods to
+ * simplify processing of input parameters.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public abstract class AbstractCommand implements Command {
+
+ //
+ // PROTECTED METHODS
+ //
+
+ /**
+ * Returns a String value from the given simple field set.
+ *
+ * @param simpleFieldSet
+ * The simple field set to get the value from
+ * @param key
+ * The key of the value
+ * @return The String value
+ * @throws FcpException
+ * if there is no value for the given key in the simple field
+ * set, or the value can not be converted to a String
+ */
+ protected String getString(SimpleFieldSet simpleFieldSet, String key) throws FcpException {
+ try {
+ return simpleFieldSet.getString(key);
+ } catch (FSParseException fspe1) {
+ throw new FcpException("Could not get parameter “" + key + "” as String.", fspe1);
+ }
+ }
+
+ /**
+ * Returns an int value from the given simple field set.
+ *
+ * @param simpleFieldSet
+ * The simple field set to get the value from
+ * @param key
+ * The key of the value
+ * @return The int value
+ * @throws FcpException
+ * if there is no value for the given key in the simple field
+ * set, or the value can not be converted to an int
+ */
+ protected int getInt(SimpleFieldSet simpleFieldSet, String key) throws FcpException {
+ try {
+ return simpleFieldSet.getInt(key);
+ } catch (FSParseException fspe1) {
+ throw new FcpException("Could not get parameter “" + key + "” as int.", fspe1);
+ }
+ }
+
+ /**
+ * Returns an int value from the given simple field set, returning a default
+ * value if the value can not be found or converted.
+ *
+ * @param simpleFieldSet
+ * The simple field set to get the value from
+ * @param key
+ * The key of the value
+ * @param defaultValue
+ * The default value
+ * @return The int value
+ */
+ protected int getInt(SimpleFieldSet simpleFieldSet, String key, int defaultValue) {
+ return simpleFieldSet.getInt(key, defaultValue);
+ }
+
+ /**
+ * Returns a boolean value from the given simple field set.
+ *
+ * @param simpleFieldSet
+ * The simple field set to get the value from
+ * @param key
+ * The key of the value
+ * @return The boolean value
+ * @throws FcpException
+ * if there is no value for the given key in the simple field
+ * set, or the value can not be converted to a boolean
+ */
+ protected boolean getBoolean(SimpleFieldSet simpleFieldSet, String key) throws FcpException {
+ try {
+ return simpleFieldSet.getBoolean(key);
+ } catch (FSParseException fspe1) {
+ throw new FcpException("Could not get parameter “" + key + "” as boolean.", fspe1);
+ }
+ }
+
+ /**
+ * Returns a boolean value from the given simple field set, returning a
+ * default value if the value can not be found or converted.
+ *
+ * @param simpleFieldSet
+ * The simple field set to get the value from
+ * @param key
+ * The key of the value
+ * @param defaultValue
+ * The default value
+ * @return The boolean value
+ */
+ protected boolean getBoolean(SimpleFieldSet simpleFieldSet, String key, boolean defaultValue) {
+ return simpleFieldSet.getBoolean(key, defaultValue);
+ }
+
+}
--- /dev/null
+/*
+ * Sone - Command.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.fcp;
+
+import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
+
+/**
+ * Implementation of an FCP interface for other clients or plugins to
+ * communicate with Sone.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Command {
+
+ /**
+ * Executes the command, returning a reply that will be sent back to the
+ * requesting plugin.
+ *
+ * @param parameters
+ * The parameters of the comand
+ * @param data
+ * The data of the command (may be {@code null})
+ * @param accessType
+ * The access type
+ * @return A reply to send back to the plugin
+ * @throws FcpException
+ * if an error processing the parameters occurs
+ */
+ public Response execute(SimpleFieldSet parameters, Bucket data, AccessType accessType) throws FcpException;
+
+ /**
+ * The access type of the request.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ public static enum AccessType {
+
+ /** Access from another plugin. */
+ DIRECT,
+
+ /** Access via restricted FCP. */
+ RESTRICTED_FCP,
+
+ /** Access via FCP with full access. */
+ FULL_FCP,
+
+ }
+
+ /**
+ * Interface for command replies.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ public static class Response {
+
+ /** The message name of the reponse. */
+ private final String messageName;
+
+ /** The reply parameters. */
+ private final SimpleFieldSet replyParameters;
+
+ /** The reply data, may be {@code null}. */
+ private final byte[] data;
+
+ /** The data bucket, may be {@code null}. */
+ private final Bucket bucket;
+
+ /**
+ * Creates a new reply with the given parameters.
+ *
+ * @param messageName
+ * The message name
+ * @param replyParameters
+ * The reply parameters
+ */
+ public Response(String messageName, SimpleFieldSet replyParameters) {
+ this(messageName, replyParameters, null, null);
+ }
+
+ /**
+ * Creates a new reply with the given parameters.
+ *
+ * @param messageName
+ * The message name
+ * @param replyParameters
+ * The reply parameters
+ * @param data
+ * The data of the reply (may be {@code null})
+ */
+ public Response(String messageName, SimpleFieldSet replyParameters, byte[] data) {
+ this(messageName, replyParameters, data, null);
+ }
+
+ /**
+ * Creates a new reply with the given parameters.
+ *
+ * @param messageName
+ * The message name
+ * @param replyParameters
+ * The reply parameters
+ * @param bucket
+ * The bucket of the reply (may be {@code null})
+ */
+ public Response(String messageName, SimpleFieldSet replyParameters, Bucket bucket) {
+ this(messageName, replyParameters, null, bucket);
+ }
+
+ /**
+ * Creates a new reply with the given parameters.
+ *
+ * @param messageName
+ * The message name
+ * @param replyParameters
+ * The reply parameters
+ * @param data
+ * The data of the reply (may be {@code null})
+ * @param bucket
+ * The bucket of the reply (may be {@code null})
+ */
+ private Response(String messageName, SimpleFieldSet replyParameters, byte[] data, Bucket bucket) {
+ this.messageName = messageName;
+ this.replyParameters = replyParameters;
+ this.data = data;
+ this.bucket = bucket;
+ }
+
+ /**
+ * Returns the reply parameters.
+ *
+ * @return The reply parameters
+ */
+ public SimpleFieldSet getReplyParameters() {
+ return new SimpleFieldSetBuilder(replyParameters).put("Message", messageName).get();
+ }
+
+ /**
+ * Returns whether the reply has reply data.
+ *
+ * @see #getData()
+ * @return {@code true} if this reply has data, {@code false} otherwise
+ */
+ public boolean hasData() {
+ return data != null;
+ }
+
+ /**
+ * Returns the data of the reply.
+ *
+ * @return The data of the reply
+ */
+ public byte[] getData() {
+ return data;
+ }
+
+ /**
+ * Returns whether the reply has a data bucket.
+ *
+ * @see #getBucket()
+ * @return {@code true} if the reply has a data bucket, {@code false}
+ * otherwise
+ */
+ public boolean hasBucket() {
+ return bucket != null;
+ }
+
+ /**
+ * Returns the data bucket of the reply.
+ *
+ * @return The data bucket of the reply
+ */
+ public Bucket getBucket() {
+ return bucket;
+ }
+
+ }
+
+ /**
+ * Response implementation that can return an error message and an optional
+ * error code.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ public class ErrorResponse extends Response {
+
+ /**
+ * Creates a new error response with the given message.
+ *
+ * @param message
+ * The error message
+ */
+ public ErrorResponse(String message) {
+ super("Error", new SimpleFieldSetBuilder().put("ErrorMessage", message).get());
+ }
+
+ /**
+ * Creates a new error response with the given code and message.
+ *
+ * @param code
+ * The error code
+ * @param message
+ * The error message
+ */
+ public ErrorResponse(int code, String message) {
+ super("Error", new SimpleFieldSetBuilder().put("ErrorMessage", message).put("ErrorCode", code).get());
+ }
+
+ }
+
+}
--- /dev/null
+/*
+ * Sone - FcpException.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.freenet.fcp;
+
+/**
+ * Base exception for FCP communication.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FcpException extends Exception {
+
+ /**
+ * Creates a new FCP exception.
+ */
+ public FcpException() {
+ super();
+ }
+
+ /**
+ * Creates a new FCP exception.
+ *
+ * @param message
+ * The message of the exception
+ */
+ public FcpException(String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new FCP exception.
+ *
+ * @param cause
+ * The cause of the exception
+ */
+ public FcpException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Creates a new FCP exception.
+ *
+ * @param message
+ * The message of the exception
+ * @param cause
+ * The cause of the exception
+ */
+ public FcpException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
import java.util.EventListener;
-
import freenet.support.SimpleFieldSet;
import freenet.support.api.Bucket;
private final Map<String, String> properties = Collections.synchronizedMap(new HashMap<String, String>());
/** Cached trust. */
+ /* synchronize on itself. */
private final WritableCache<OwnIdentity, Trust> trustCache = new MemoryCache<OwnIdentity, Trust>(new ValueRetriever<OwnIdentity, Trust>() {
@Override
@Override
public Trust getTrust(OwnIdentity ownIdentity) {
try {
- return trustCache.get(ownIdentity);
+ synchronized (trustCache) {
+ return trustCache.get(ownIdentity);
+ }
} catch (CacheException ce1) {
logger.log(Level.WARNING, "Could not get trust for OwnIdentity: " + ownIdentity, ce1);
return null;
* The trust received for this identity
*/
void setTrustPrivate(OwnIdentity ownIdentity, Trust trust) {
- trustCache.put(ownIdentity, trust);
+ synchronized (trustCache) {
+ trustCache.put(ownIdentity, trust);
+ }
}
//
Map<OwnIdentity, Map<String, Identity>> oldIdentities = Collections.emptyMap();
while (!shouldStop()) {
Map<OwnIdentity, Map<String, Identity>> currentIdentities = new HashMap<OwnIdentity, Map<String, Identity>>();
+ @SuppressWarnings("hiding")
Map<String, OwnIdentity> currentOwnIdentities = new HashMap<String, OwnIdentity>();
+ Set<OwnIdentity> ownIdentities = null;
+ boolean identitiesLoaded = false;
try {
/* get all identities with the wanted context from WoT. */
- Set<OwnIdentity> ownIdentities = webOfTrustConnector.loadAllOwnIdentities();
+ ownIdentities = webOfTrustConnector.loadAllOwnIdentities();
- /* check for changes. */
- for (OwnIdentity ownIdentity : ownIdentities) {
- currentOwnIdentities.put(ownIdentity.getId(), ownIdentity);
- }
- checkOwnIdentities(currentOwnIdentities);
-
- /* now filter for context and get all identities. */
+ /* load trusted identities. */
for (OwnIdentity ownIdentity : ownIdentities) {
if ((context != null) && !ownIdentity.hasContext(context)) {
continue;
}
+ currentOwnIdentities.put(ownIdentity.getId(), ownIdentity);
Set<Identity> trustedIdentities = webOfTrustConnector.loadTrustedIdentities(ownIdentity, context);
Map<String, Identity> identities = new HashMap<String, Identity>();
for (Identity identity : trustedIdentities) {
identities.put(identity.getId(), identity);
}
+ }
+ identitiesLoaded = true;
+ } catch (WebOfTrustException wote1) {
+ logger.log(Level.WARNING, "WoT has disappeared!", wote1);
+ }
+
+ if (identitiesLoaded) {
+
+ /* check for changes. */
+ checkOwnIdentities(currentOwnIdentities);
+
+ /* now check for changes in remote identities. */
+ for (OwnIdentity ownIdentity : currentOwnIdentities.values()) {
/* find new identities. */
for (Identity currentIdentity : currentIdentities.get(ownIdentity).values()) {
}
}
}
-
- /* remember the current set of identities. */
- oldIdentities = currentIdentities;
}
- } catch (WebOfTrustException wote1) {
- logger.log(Level.WARNING, "WoT has disappeared!", wote1);
+ /* remember the current set of identities. */
+ oldIdentities = currentIdentities;
}
/* wait a minute before checking again. */
* if the own identities can not be loaded
*/
public Set<OwnIdentity> loadAllOwnIdentities() throws WebOfTrustException {
+ @SuppressWarnings("hiding")
Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetOwnIdentities").get());
SimpleFieldSet fields = reply.getFields();
int ownIdentityCounter = -1;
* if an error occured talking to the Web of Trust plugin
*/
public Set<Identity> loadTrustedIdentities(OwnIdentity ownIdentity, String context) throws PluginException {
+ @SuppressWarnings("hiding")
Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetIdentitiesByScore").put("TreeOwner", ownIdentity.getId()).put("Selection", "+").put("Context", (context == null) ? "" : context).get());
SimpleFieldSet fields = reply.getFields();
Set<Identity> identities = new HashSet<Identity>();
* if an error occured talking to the Web of Trust plugin
*/
public String getProperty(Identity identity, String name) throws PluginException {
+ @SuppressWarnings("hiding")
Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetProperty").put("Identity", identity.getId()).put("Property", name).get());
return reply.getFields().get("Property");
}
* {@inheritDoc}
*/
@Override
- public void receivedReply(PluginConnector pluginConnector, SimpleFieldSet fields, Bucket data) {
+ public void receivedReply(@SuppressWarnings("hiding") PluginConnector pluginConnector, SimpleFieldSet fields, Bucket data) {
String messageName = fields.get("Message");
logger.log(Level.FINEST, "Received Reply from Plugin: " + messageName);
synchronized (reply) {
/*
- * FreenetSone - SonePlugin.java - Copyright © 2010 David Roden
+ * Sone - SonePlugin.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
import net.pterodactylus.sone.core.Core;
import net.pterodactylus.sone.core.FreenetInterface;
+import net.pterodactylus.sone.fcp.FcpInterface;
import net.pterodactylus.sone.freenet.PluginStoreConfigurationBackend;
import net.pterodactylus.sone.freenet.plugin.PluginConnector;
import net.pterodactylus.sone.freenet.wot.IdentityManager;
import freenet.l10n.PluginL10n;
import freenet.pluginmanager.FredPlugin;
import freenet.pluginmanager.FredPluginBaseL10n;
+import freenet.pluginmanager.FredPluginFCP;
import freenet.pluginmanager.FredPluginL10n;
import freenet.pluginmanager.FredPluginThreadless;
import freenet.pluginmanager.FredPluginVersioned;
+import freenet.pluginmanager.PluginReplySender;
import freenet.pluginmanager.PluginRespirator;
+import freenet.support.SimpleFieldSet;
+import freenet.support.api.Bucket;
/**
* This class interfaces with Freenet. It is the class that is loaded by the
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
-public class SonePlugin implements FredPlugin, FredPluginL10n, FredPluginBaseL10n, FredPluginThreadless, FredPluginVersioned {
+public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, FredPluginBaseL10n, FredPluginThreadless, FredPluginVersioned {
static {
/* initialize logging. */
}
/** The version. */
- public static final Version VERSION = new Version(0, 6, 1);
+ public static final Version VERSION = new Version(0, 6, 7);
/** The logger. */
private static final Logger logger = Logging.getLogger(SonePlugin.class);
/** The web interface. */
private WebInterface webInterface;
+ /** The FCP interface. */
+ private FcpInterface fcpInterface;
+
/** The l10n helper. */
private PluginL10n l10n;
* {@inheritDoc}
*/
@Override
- public void runPlugin(PluginRespirator pluginRespirator) {
+ public void runPlugin(@SuppressWarnings("hiding") PluginRespirator pluginRespirator) {
this.pluginRespirator = pluginRespirator;
/* create a configuration. */
webInterface = new WebInterface(this);
core.addCoreListener(webInterface);
+ /* create FCP interface. */
+ fcpInterface = new FcpInterface(core);
+ core.setFcpInterface(fcpInterface);
+
/* create the identity manager. */
identityManager.addIdentityListener(core);
}
//
+ // INTERFACE FredPluginFCP
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void handle(PluginReplySender pluginReplySender, SimpleFieldSet parameters, Bucket data, int accessType) {
+ fcpInterface.handle(pluginReplySender, parameters, data, accessType);
+ }
+
+ //
// INTERFACE FredPluginL10n
//
}
/**
- * Sets the elements to show in this notification.
+ * Sets the elements to show in this notification. This method will not call
+ * {@link #touch()}.
*
* @param elements
* The elements to show
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Reply;
import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.freenet.wot.OwnIdentity;
+import net.pterodactylus.sone.freenet.wot.Trust;
import net.pterodactylus.util.notify.Notification;
+import net.pterodactylus.util.validation.Validation;
/**
* Filter for {@link ListNotification}s.
* The current Sone, or {@code null} if not logged in
* @return The filtered notifications
*/
- public static List<Notification> filterNotifications(List<Notification> notifications, Sone currentSone) {
- ListNotification<Post> newPostNotification = getNotification(notifications, "new-post-notification", Post.class);
- if (newPostNotification != null) {
- ListNotification<Post> filteredNotification = filterNewPostNotification(newPostNotification, currentSone);
- int notificationIndex = notifications.indexOf(newPostNotification);
- if (filteredNotification == null) {
- notifications.remove(notificationIndex);
- } else {
- notifications.set(notificationIndex, filteredNotification);
- }
- }
- ListNotification<Reply> newReplyNotification = getNotification(notifications, "new-replies-notification", Reply.class);
- if (newReplyNotification != null) {
- ListNotification<Reply> filteredNotification = filterNewReplyNotification(newReplyNotification, currentSone);
- int notificationIndex = notifications.indexOf(newReplyNotification);
- if (filteredNotification == null) {
- notifications.remove(notificationIndex);
+ @SuppressWarnings("unchecked")
+ public static List<Notification> filterNotifications(Collection<? extends Notification> notifications, Sone currentSone) {
+ List<Notification> filteredNotifications = new ArrayList<Notification>();
+ for (Notification notification : notifications) {
+ if (notification.getId().equals("new-post-notification")) {
+ ListNotification<Post> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, currentSone, true);
+ if (filteredNotification != null) {
+ filteredNotifications.add(filteredNotification);
+ }
+ } else if (notification.getId().equals("new-reply-notification")) {
+ ListNotification<Reply> filteredNotification = filterNewReplyNotification((ListNotification<Reply>) notification, currentSone);
+ if (filteredNotification != null) {
+ filteredNotifications.add(filteredNotification);
+ }
+ } else if (notification.getId().equals("mention-notification")) {
+ ListNotification<Post> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, null, false);
+ if (filteredNotification != null) {
+ filteredNotifications.add(filteredNotification);
+ }
} else {
- notifications.set(notificationIndex, filteredNotification);
+ filteredNotifications.add(notification);
}
}
- return notifications;
+ return filteredNotifications;
}
/**
* Filters the new posts of the given notification. If {@code currentSone}
- * is {@code null}, {@code null} is returned and the notification is
- * subsequently removed. Otherwise only posts that are posted by friend
- * Sones of the given Sone are retained; all other posts are removed.
+ * is {@code null} and {@code soneRequired} is {@code true}, {@code null} is
+ * returned and the notification is subsequently removed. Otherwise only
+ * posts that are posted by friend Sones of the given Sone are retained; all
+ * other posts are removed.
*
* @param newPostNotification
* The new-post notification
* @param currentSone
* The current Sone, or {@code null} if not logged in
+ * @param soneRequired
+ * Whether a non-{@code null} Sone in {@code currentSone} is
+ * required
* @return The filtered new-post notification, or {@code null} if the
* notification should be removed
*/
- private static ListNotification<Post> filterNewPostNotification(ListNotification<Post> newPostNotification, Sone currentSone) {
- if (currentSone == null) {
+ public static ListNotification<Post> filterNewPostNotification(ListNotification<Post> newPostNotification, Sone currentSone, boolean soneRequired) {
+ if (soneRequired && (currentSone == null)) {
return null;
}
List<Post> newPosts = new ArrayList<Post>();
for (Post post : newPostNotification.getElements()) {
- if (currentSone.hasFriend(post.getSone().getId()) || currentSone.equals(post.getSone()) || currentSone.equals(post.getRecipient())) {
+ if (isPostVisible(currentSone, post)) {
newPosts.add(post);
}
}
}
ListNotification<Post> filteredNotification = new ListNotification<Post>(newPostNotification);
filteredNotification.setElements(newPosts);
+ filteredNotification.setLastUpdateTime(newPostNotification.getLastUpdatedTime());
return filteredNotification;
}
* @return The filtered new-reply notification, or {@code null} if the
* notification should be removed
*/
- private static ListNotification<Reply> filterNewReplyNotification(ListNotification<Reply> newReplyNotification, Sone currentSone) {
+ public static ListNotification<Reply> filterNewReplyNotification(ListNotification<Reply> newReplyNotification, Sone currentSone) {
if (currentSone == null) {
return null;
}
List<Reply> newReplies = new ArrayList<Reply>();
for (Reply reply : newReplyNotification.getElements()) {
- if (((reply.getPost().getSone() != null) && currentSone.hasFriend(reply.getPost().getSone().getId())) || currentSone.equals(reply.getPost().getSone()) || currentSone.equals(reply.getPost().getRecipient())) {
+ if (isReplyVisible(currentSone, reply)) {
newReplies.add(reply);
}
}
}
ListNotification<Reply> filteredNotification = new ListNotification<Reply>(newReplyNotification);
filteredNotification.setElements(newReplies);
+ filteredNotification.setLastUpdateTime(newReplyNotification.getLastUpdatedTime());
return filteredNotification;
}
/**
- * Finds the notification with the given ID in the list of notifications and
- * returns it.
+ * Checks whether a post is visible to the given Sone. A post is not
+ * considered visible if one of the following statements is true:
+ * <ul>
+ * <li>The post does not have a Sone.</li>
+ * <li>The post’s {@link Post#getTime() time} is in the future.</li>
+ * </ul>
+ * <p>
+ * If {@code post} is not {@code null} more checks are performed, and the
+ * post will be invisible if:
+ * </p>
+ * <ul>
+ * <li>The Sone of the post is not the given Sone, the given Sone does not
+ * follow the post’s Sone, and the given Sone is not the recipient of the
+ * post.</li>
+ * <li>The trust relationship between the two Sones can not be retrieved.</li>
+ * <li>The given Sone has explicitely assigned negative trust to the post’s
+ * Sone.</li>
+ * <li>The given Sone has not explicitely assigned negative trust to the
+ * post’s Sone but the implicit trust is negative.</li>
+ * </ul>
+ * If none of these statements is true the post is considered visible.
*
- * @param <T>
- * The type of the item in the notification
- * @param notifications
- * The notification to search
- * @param notificationId
- * The ID of the requested notification
- * @param notificationElementClass
- * The class of the notification item
- * @return The requested notification, or {@code null} if no notification
- * with the given ID could be found
+ * @param sone
+ * The Sone that checks for a post’s visibility (may be
+ * {@code null} to skip Sone-specific checks, such as trust)
+ * @param post
+ * The post to check for visibility
+ * @return {@code true} if the post is considered visible, {@code false}
+ * otherwise
*/
- @SuppressWarnings("unchecked")
- private static <T> ListNotification<T> getNotification(Collection<? extends Notification> notifications, String notificationId, Class<T> notificationElementClass) {
- for (Notification notification : notifications) {
- if (!notificationId.equals(notification.getId())) {
- continue;
+ public static boolean isPostVisible(Sone sone, Post post) {
+ Validation.begin().isNotNull("Post", post).check();
+ Sone postSone = post.getSone();
+ if (postSone == null) {
+ return false;
+ }
+ if (sone != null) {
+ Trust trust = postSone.getIdentity().getTrust((OwnIdentity) sone.getIdentity());
+ if (trust != null) {
+ if ((trust.getExplicit() != null) && (trust.getExplicit() < 0)) {
+ return false;
+ }
+ if ((trust.getExplicit() == null) && (trust.getImplicit() != null) && (trust.getImplicit() < 0)) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ if ((!postSone.equals(sone)) && !sone.hasFriend(postSone.getId()) && !sone.equals(post.getRecipient())) {
+ return false;
}
- return (ListNotification<T>) notification;
}
- return null;
+ if (post.getTime() > System.currentTimeMillis()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks whether a reply is visible to the given Sone. A reply is not
+ * considered visible if one of the following statements is true:
+ * <ul>
+ * <li>The reply does not have a post.</li>
+ * <li>The reply’s post does not have a Sone.</li>
+ * <li>The Sone of the reply’s post is not the given Sone, the given Sone
+ * does not follow the reply’s post’s Sone, and the given Sone is not the
+ * recipient of the reply’s post.</li>
+ * <li>The trust relationship between the two Sones can not be retrieved.</li>
+ * <li>The given Sone has explicitely assigned negative trust to the post’s
+ * Sone.</li>
+ * <li>The given Sone has not explicitely assigned negative trust to the
+ * reply’s post’s Sone but the implicit trust is negative.</li>
+ * <li>The reply’s post’s {@link Post#getTime() time} is in the future.</li>
+ * <li>The reply’s {@link Reply#getTime() time} is in the future.</li>
+ * </ul>
+ * If none of these statements is true the reply is considered visible.
+ *
+ * @param sone
+ * The Sone that checks for a post’s visibility (may be
+ * {@code null} to skip Sone-specific checks, such as trust)
+ * @param reply
+ * The reply to check for visibility
+ * @return {@code true} if the reply is considered visible, {@code false}
+ * otherwise
+ */
+ public static boolean isReplyVisible(Sone sone, Reply reply) {
+ Validation.begin().isNotNull("Reply", reply).check();
+ Post post = reply.getPost();
+ if (post == null) {
+ return false;
+ }
+ if (!isPostVisible(sone, post)) {
+ return false;
+ }
+ if (reply.getTime() > System.currentTimeMillis()) {
+ return false;
+ }
+ return true;
}
}
import java.util.Map;
-import net.pterodactylus.sone.web.page.Page.Request;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Plugin;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Request;
/**
* Extracts a page number from a {@link Request}’s parameters and stores it in
pageKey = "page";
}
- Request request = (Request) templateContext.get(requestKey);
+ FreenetRequest request = (FreenetRequest) templateContext.get(requestKey);
String pageString = request.getHttpRequest().getParam(parameter);
int page = 0;
try {
+++ /dev/null
-/*
- * Sone - NotificationManagerAccessor.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
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.template;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.notify.ListNotificationFilters;
-import net.pterodactylus.util.notify.Notification;
-import net.pterodactylus.util.notify.NotificationManager;
-import net.pterodactylus.util.template.ReflectionAccessor;
-import net.pterodactylus.util.template.TemplateContext;
-
-/**
- * Adds additional properties to a {@link NotificationManager}.
- * <dl>
- * <dd>all</dd>
- * <dt>Returns all notifications, sorted by creation time, oldest first.</dt>
- * <dd>new</dd>
- * <dt>Returns all changed notifications, sorted by last updated time, newest
- * first.</dt>
- * </dl>
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class NotificationManagerAccessor extends ReflectionAccessor {
-
- /**
- * {@inheritDoc}
- */
- @Override
- public Object get(TemplateContext templateContext, Object object, String member) {
- NotificationManager notificationManager = (NotificationManager) object;
- if ("all".equals(member)) {
- List<Notification> notifications = ListNotificationFilters.filterNotifications(new ArrayList<Notification>(notificationManager.getNotifications()), (Sone) templateContext.get("currentSone"));
- Collections.sort(notifications, Notification.CREATED_TIME_SORTER);
- return notifications;
- }
- return super.get(templateContext, object, member);
- }
-
-}
import java.io.IOException;
import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import net.pterodactylus.sone.core.Core;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.text.FreenetLinkParser;
-import net.pterodactylus.sone.text.FreenetLinkParserContext;
+import net.pterodactylus.sone.text.FreenetLinkPart;
+import net.pterodactylus.sone.text.LinkPart;
+import net.pterodactylus.sone.text.Part;
+import net.pterodactylus.sone.text.PlainTextPart;
+import net.pterodactylus.sone.text.PostPart;
+import net.pterodactylus.sone.text.SonePart;
+import net.pterodactylus.sone.text.SoneTextParser;
+import net.pterodactylus.sone.text.SoneTextParserContext;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
import net.pterodactylus.util.template.TemplateContextFactory;
+import net.pterodactylus.util.template.TemplateParser;
/**
- * Filter that filters a given text through a {@link FreenetLinkParser}.
+ * Filter that filters a given text through a {@link SoneTextParser}.
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
private final Core core;
/** The link parser. */
- private final FreenetLinkParser linkParser;
+ private final SoneTextParser soneTextParser;
+
+ /** The template context factory. */
+ private final TemplateContextFactory templateContextFactory;
+
+ /** The template for {@link PlainTextPart}s. */
+ private final Template plainTextTemplate = TemplateParser.parse(new StringReader("<%text|html>"));
+
+ /** The template for {@link FreenetLinkPart}s. */
+ private final Template linkTemplate = TemplateParser.parse(new StringReader("<a class=\"<%cssClass|html>\" href=\"<%link|html>\" title=\"<%title|html>\"><%text|html></a>"));
/**
- * Creates a new filter that runs its input through a
- * {@link FreenetLinkParser}.
+ * Creates a new filter that runs its input through a {@link SoneTextParser}
+ * .
*
* @param core
* The core
* @param templateContextFactory
* The context factory for rendering the parts
+ * @param soneTextParser
+ * The Sone text parser
*/
- public ParserFilter(Core core, TemplateContextFactory templateContextFactory) {
+ public ParserFilter(Core core, TemplateContextFactory templateContextFactory, SoneTextParser soneTextParser) {
this.core = core;
- linkParser = new FreenetLinkParser(core, templateContextFactory);
+ this.templateContextFactory = templateContextFactory;
+ this.soneTextParser = soneTextParser;
}
/**
@Override
public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
String text = String.valueOf(data);
+ int length = -1;
+ try {
+ length = Integer.parseInt(parameters.get("length"));
+ } catch (NumberFormatException nfe1) {
+ /* ignore. */
+ }
+ if ((length == -1) && (parameters.get("length") != null)) {
+ try {
+ length = Integer.parseInt(String.valueOf(templateContext.get(parameters.get("length"))));
+ } catch (NumberFormatException nfe1) {
+ /* ignore. */
+ }
+ }
String soneKey = parameters.get("sone");
if (soneKey == null) {
soneKey = "sone";
if (sone == null) {
sone = core.getSone(soneKey, false);
}
- FreenetLinkParserContext context = new FreenetLinkParserContext(sone);
+ FreenetRequest request = (FreenetRequest) templateContext.get("request");
+ SoneTextParserContext context = new SoneTextParserContext(request, sone);
+ StringWriter parsedTextWriter = new StringWriter();
try {
- return linkParser.parse(context, new StringReader(text));
+ Iterable<Part> parts = soneTextParser.parse(context, new StringReader(text));
+ if (length > -1) {
+ List<Part> shortenedParts = new ArrayList<Part>();
+ for (Part part : parts) {
+ if (part instanceof PlainTextPart) {
+ String longText = ((PlainTextPart) part).getText();
+ if (length >= longText.length()) {
+ shortenedParts.add(part);
+ } else {
+ shortenedParts.add(new PlainTextPart(longText.substring(0, length) + "…"));
+ }
+ length -= longText.length();
+ } else if (part instanceof LinkPart) {
+ shortenedParts.add(part);
+ length -= ((LinkPart) part).getText().length();
+ } else {
+ shortenedParts.add(part);
+ }
+ if (length <= 0) {
+ break;
+ }
+ }
+ parts = shortenedParts;
+ }
+ render(parsedTextWriter, parts);
} catch (IOException ioe1) {
- /* no exceptions in a StringReader, ignore. */
+ /* no exceptions in a StringReader or StringWriter, ignore. */
+ }
+ return parsedTextWriter.toString();
+ }
+
+ //
+ // PRIVATE METHODS
+ //
+
+ /**
+ * Renders the given parts.
+ *
+ * @param writer
+ * The writer to render the parts to
+ * @param parts
+ * The parts to render
+ */
+ private void render(Writer writer, Iterable<Part> parts) {
+ for (Part part : parts) {
+ render(writer, part);
+ }
+ }
+
+ /**
+ * Renders the given part.
+ *
+ * @param writer
+ * The writer to render the part to
+ * @param part
+ * The part to render
+ */
+ @SuppressWarnings("unchecked")
+ private void render(Writer writer, Part part) {
+ if (part instanceof PlainTextPart) {
+ render(writer, (PlainTextPart) part);
+ } else if (part instanceof FreenetLinkPart) {
+ render(writer, (FreenetLinkPart) part);
+ } else if (part instanceof LinkPart) {
+ render(writer, (LinkPart) part);
+ } else if (part instanceof SonePart) {
+ render(writer, (SonePart) part);
+ } else if (part instanceof PostPart) {
+ render(writer, (PostPart) part);
+ } else if (part instanceof Iterable<?>) {
+ render(writer, (Iterable<Part>) part);
+ }
+ }
+
+ /**
+ * Renders the given plain-text part.
+ *
+ * @param writer
+ * The writer to render the part to
+ * @param plainTextPart
+ * The part to render
+ */
+ private void render(Writer writer, PlainTextPart plainTextPart) {
+ TemplateContext templateContext = templateContextFactory.createTemplateContext();
+ templateContext.set("text", plainTextPart.getText());
+ plainTextTemplate.render(templateContext, writer);
+ }
+
+ /**
+ * Renders the given freenet link part.
+ *
+ * @param writer
+ * The writer to render the part to
+ * @param freenetLinkPart
+ * The part to render
+ */
+ private void render(Writer writer, FreenetLinkPart freenetLinkPart) {
+ renderLink(writer, "/" + freenetLinkPart.getLink(), freenetLinkPart.getText(), freenetLinkPart.getTitle(), freenetLinkPart.isTrusted() ? "freenet-trusted" : "freenet");
+ }
+
+ /**
+ * Renders the given link part.
+ *
+ * @param writer
+ * The writer to render the part to
+ * @param linkPart
+ * The part to render
+ */
+ private void render(Writer writer, LinkPart linkPart) {
+ renderLink(writer, "/?_CHECKED_HTTP_=" + linkPart.getLink(), linkPart.getText(), linkPart.getTitle(), "internet");
+ }
+
+ /**
+ * Renders the given Sone part.
+ *
+ * @param writer
+ * The writer to render the part to
+ * @param sonePart
+ * The part to render
+ */
+ private void render(Writer writer, SonePart sonePart) {
+ renderLink(writer, "viewSone.html?sone=" + sonePart.getSone().getId(), SoneAccessor.getNiceName(sonePart.getSone()), SoneAccessor.getNiceName(sonePart.getSone()), "in-sone");
+ }
+
+ /**
+ * Renders the given post part.
+ *
+ * @param writer
+ * The writer to render the part to
+ * @param postPart
+ * The part to render
+ */
+ private void render(Writer writer, PostPart postPart) {
+ renderLink(writer, "viewPost.html?post=" + postPart.getPost().getId(), getExcerpt(postPart.getPost().getText(), 20), SoneAccessor.getNiceName(postPart.getPost().getSone()), "in-sone");
+ }
+
+ /**
+ * Renders the given link.
+ *
+ * @param writer
+ * The writer to render the link to
+ * @param link
+ * The link to render
+ * @param text
+ * The text of the link
+ * @param title
+ * The title of the link
+ * @param cssClass
+ * The CSS class of the link
+ */
+ private void renderLink(Writer writer, String link, String text, String title, String cssClass) {
+ TemplateContext templateContext = templateContextFactory.createTemplateContext();
+ templateContext.set("cssClass", cssClass);
+ templateContext.set("link", link);
+ templateContext.set("text", text);
+ templateContext.set("title", title);
+ linkTemplate.render(templateContext, writer);
+ }
+
+ //
+ // STATIC METHODS
+ //
+
+ /**
+ * Returns up to {@code length} characters from the given text, appending
+ * “…” if the text is longer.
+ *
+ * @param text
+ * The text to get an excerpt from
+ * @param length
+ * The maximum length of the excerpt (without the ellipsis)
+ * @return The excerpt of the text
+ */
+ private static String getExcerpt(String text, int length) {
+ String filteredText = text.replaceAll("(\r\n)+", "\r\n").replaceAll("\n+", "\n").replace("\r\n", " ").replace('\n', ' ');
+ if (filteredText.length() > length) {
+ return filteredText.substring(0, length) + "…";
}
- return null;
+ return filteredText;
}
}
import java.util.Map;
import java.util.Map.Entry;
-import net.pterodactylus.sone.web.page.Page.Request;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Filter;
import net.pterodactylus.util.template.TemplateContext;
/**
- * This filter expects a {@link Request} as input and outputs a {@link URI} that
- * is modified by the parameters. The name of the parameter is handed in as
- * “name”, the value may either be stored in “value”, or in a template variable
- * whose key is stored in “key”.
+ * This filter expects a {@link FreenetRequest} as input and outputs a
+ * {@link URI} that is modified by the parameters. The name of the parameter is
+ * handed in as “name”, the value may either be stored in “value”, or in a
+ * template variable whose key is stored in “key”.
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
*/
@Override
public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
- Request request = (Request) data;
+ FreenetRequest request = (FreenetRequest) data;
String name = parameters.get("name");
String nameKey = parameters.get("nameKey");
if (nameKey != null) {
import net.pterodactylus.sone.data.Profile;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.freenet.wot.Trust;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.ajax.GetTimesAjaxPage;
import net.pterodactylus.util.logging.Logging;
import net.pterodactylus.util.template.Accessor;
import net.pterodactylus.util.template.ReflectionAccessor;
return core.isNewSone(sone.getId());
} else if (member.equals("locked")) {
return core.isLocked(sone);
+ } else if (member.equals("lastUpdatedText")) {
+ return GetTimesAjaxPage.getTime((WebInterface) templateContext.get("webInterface"), sone.getTime());
} else if (member.equals("trust")) {
Sone currentSone = (Sone) templateContext.get("currentSone");
if (currentSone == null) {
--- /dev/null
+/*
+ * Sone - UniqueElementFilter.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.template;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * Filter that reduces a collection to a {@link Set}, removing duplicates.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UniqueElementFilter implements Filter {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
+ if (!(data instanceof Collection<?>)) {
+ return data;
+ }
+ return new HashSet<Object>((Collection<?>) data);
+ }
+
+}
+++ /dev/null
-/*
- * Sone - FreenetLinkParser.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
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.text;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.Reader;
-import java.io.StringReader;
-import java.net.MalformedURLException;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.template.SoneAccessor;
-import net.pterodactylus.util.logging.Logging;
-import net.pterodactylus.util.template.TemplateContextFactory;
-import net.pterodactylus.util.template.TemplateParser;
-import freenet.keys.FreenetURI;
-
-/**
- * {@link Parser} implementation that can recognize Freenet URIs.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class FreenetLinkParser implements Parser<FreenetLinkParserContext> {
-
- /** The logger. */
- private static final Logger logger = Logging.getLogger(FreenetLinkParser.class);
-
- /** Pattern to detect whitespace. */
- private static final Pattern whitespacePattern = Pattern.compile("[\\u000a\u0020\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u200c\u200d\u202f\u205f\u2060\u2800\u3000]");
-
- /**
- * Enumeration for all recognized link types.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
- private enum LinkType {
-
- /** Link is a KSK. */
- KSK,
-
- /** Link is a CHK. */
- CHK,
-
- /** Link is an SSK. */
- SSK,
-
- /** Link is a USK. */
- USK,
-
- /** Link is HTTP. */
- HTTP,
-
- /** Link is HTTPS. */
- HTTPS,
-
- /** Link is a Sone. */
- SONE,
-
- /** Link is a post. */
- POST,
-
- }
-
- /** The core. */
- private final Core core;
-
- /** The template factory. */
- private final TemplateContextFactory templateContextFactory;
-
- /**
- * Creates a new freenet link parser.
- *
- * @param core
- * The core
- * @param templateContextFactory
- * The template context factory
- */
- public FreenetLinkParser(Core core, TemplateContextFactory templateContextFactory) {
- this.core = core;
- this.templateContextFactory = templateContextFactory;
- }
-
- //
- // PART METHODS
- //
-
- /**
- * {@inheritDoc}
- */
- @Override
- public Part parse(FreenetLinkParserContext context, Reader source) throws IOException {
- PartContainer parts = new PartContainer();
- BufferedReader bufferedReader = (source instanceof BufferedReader) ? (BufferedReader) source : new BufferedReader(source);
- String line;
- boolean lastLineEmpty = true;
- int emptyLines = 0;
- while ((line = bufferedReader.readLine()) != null) {
- if (line.trim().length() == 0) {
- if (lastLineEmpty) {
- continue;
- }
- parts.add(createPlainTextPart("\n"));
- ++emptyLines;
- lastLineEmpty = emptyLines == 2;
- continue;
- }
- emptyLines = 0;
- boolean lineComplete = true;
- while (line.length() > 0) {
- int nextKsk = line.indexOf("KSK@");
- int nextChk = line.indexOf("CHK@");
- int nextSsk = line.indexOf("SSK@");
- int nextUsk = line.indexOf("USK@");
- int nextHttp = line.indexOf("http://");
- int nextHttps = line.indexOf("https://");
- int nextSone = line.indexOf("sone://");
- int nextPost = line.indexOf("post://");
- if ((nextKsk == -1) && (nextChk == -1) && (nextSsk == -1) && (nextUsk == -1) && (nextHttp == -1) && (nextHttps == -1) && (nextSone == -1) && (nextPost == -1)) {
- if (lineComplete && !lastLineEmpty) {
- parts.add(createPlainTextPart("\n" + line));
- } else {
- parts.add(createPlainTextPart(line));
- }
- break;
- }
- int next = Integer.MAX_VALUE;
- LinkType linkType = null;
- if ((nextKsk > -1) && (nextKsk < next)) {
- next = nextKsk;
- linkType = LinkType.KSK;
- }
- if ((nextChk > -1) && (nextChk < next)) {
- next = nextChk;
- linkType = LinkType.CHK;
- }
- if ((nextSsk > -1) && (nextSsk < next)) {
- next = nextSsk;
- linkType = LinkType.SSK;
- }
- if ((nextUsk > -1) && (nextUsk < next)) {
- next = nextUsk;
- linkType = LinkType.USK;
- }
- if ((nextHttp > -1) && (nextHttp < next)) {
- next = nextHttp;
- linkType = LinkType.HTTP;
- }
- if ((nextHttps > -1) && (nextHttps < next)) {
- next = nextHttps;
- linkType = LinkType.HTTPS;
- }
- if ((nextSone > -1) && (nextSone < next)) {
- next = nextSone;
- linkType = LinkType.SONE;
- }
- if ((nextPost > -1) && (nextPost < next)) {
- next = nextPost;
- linkType = LinkType.POST;
- }
- if ((next >= 8) && (line.substring(next - 8, next).equals("freenet:"))) {
- next -= 8;
- line = line.substring(0, next) + line.substring(next + 8);
- }
- Matcher matcher = whitespacePattern.matcher(line);
- int nextSpace = matcher.find(next) ? matcher.start() : line.length();
- if (nextSpace > (next + 4)) {
- if (!lastLineEmpty && lineComplete) {
- parts.add(createPlainTextPart("\n" + line.substring(0, next)));
- } else {
- parts.add(createPlainTextPart(line.substring(0, next)));
- }
- String link = line.substring(next, nextSpace);
- String name = link;
- logger.log(Level.FINER, "Found link: %s", link);
- logger.log(Level.FINEST, "Next: %d, CHK: %d, SSK: %d, USK: %d", new Object[] { next, nextChk, nextSsk, nextUsk });
-
- if ((linkType == LinkType.KSK) || (linkType == LinkType.CHK) || (linkType == LinkType.SSK) || (linkType == LinkType.USK)) {
- FreenetURI uri;
- if (name.indexOf('?') > -1) {
- name = name.substring(0, name.indexOf('?'));
- }
- if (name.endsWith("/")) {
- name = name.substring(0, name.length() - 1);
- }
- try {
- uri = new FreenetURI(name);
- name = uri.lastMetaString();
- if (name == null) {
- name = uri.getDocName();
- }
- if (name == null) {
- name = link.substring(0, Math.min(9, link.length()));
- }
- boolean fromPostingSone = (context.getPostingSone() != null) && ((linkType == LinkType.SSK) || (linkType == LinkType.USK)) && link.substring(4, Math.min(link.length(), 47)).equals(context.getPostingSone().getId());
- parts.add(fromPostingSone ? createTrustedFreenetLinkPart(link, name) : createFreenetLinkPart(link, name));
- } catch (MalformedURLException mue1) {
- /* not a valid link, insert as plain text. */
- parts.add(createPlainTextPart(link));
- } catch (NullPointerException npe1) {
- /* FreenetURI sometimes throws these, too. */
- parts.add(createPlainTextPart(link));
- } catch (ArrayIndexOutOfBoundsException aioobe1) {
- /* oh, and these, too. */
- parts.add(createPlainTextPart(link));
- }
- } else if ((linkType == LinkType.HTTP) || (linkType == LinkType.HTTPS)) {
- name = link.substring(linkType == LinkType.HTTP ? 7 : 8);
- int firstSlash = name.indexOf('/');
- int lastSlash = name.lastIndexOf('/');
- if ((lastSlash - firstSlash) > 3) {
- name = name.substring(0, firstSlash + 1) + "…" + name.substring(lastSlash);
- }
- if (name.endsWith("/")) {
- name = name.substring(0, name.length() - 1);
- }
- if (((name.indexOf('/') > -1) && (name.indexOf('.') < name.lastIndexOf('.', name.indexOf('/'))) || ((name.indexOf('/') == -1) && (name.indexOf('.') < name.lastIndexOf('.')))) && name.startsWith("www.")) {
- name = name.substring(4);
- }
- if (name.indexOf('?') > -1) {
- name = name.substring(0, name.indexOf('?'));
- }
- link = "?_CHECKED_HTTP_=" + link;
- parts.add(createInternetLinkPart(link, name));
- } else if (linkType == LinkType.SONE) {
- String soneId = link.substring(7);
- Sone sone = core.getSone(soneId, false);
- if (sone != null) {
- parts.add(createInSoneLinkPart("viewSone.html?sone=" + soneId, SoneAccessor.getNiceName(sone)));
- } else {
- parts.add(createPlainTextPart(link));
- }
- } else if (linkType == LinkType.POST) {
- String postId = link.substring(7);
- Post post = core.getPost(postId, false);
- if (post != null) {
- String postText = post.getText();
- postText = postText.substring(0, Math.min(postText.length(), 20)) + "…";
- Sone postSone = post.getSone();
- parts.add(createInSoneLinkPart("viewPost.html?post=" + postId, postText, (postSone == null) ? postText : SoneAccessor.getNiceName(post.getSone())));
- } else {
- parts.add(createPlainTextPart(link));
- }
- }
- line = line.substring(nextSpace);
- } else {
- if (!lastLineEmpty && lineComplete) {
- parts.add(createPlainTextPart("\n" + line.substring(0, next + 4)));
- } else {
- parts.add(createPlainTextPart(line.substring(0, next + 4)));
- }
- line = line.substring(next + 4);
- }
- lineComplete = false;
- }
- lastLineEmpty = false;
- }
- for (int partIndex = parts.size() - 1; partIndex >= 0; --partIndex) {
- if (!parts.getPart(partIndex).toString().equals("\n")) {
- break;
- }
- parts.removePart(partIndex);
- }
- return parts;
- }
-
- //
- // PRIVATE METHODS
- //
-
- /**
- * Creates a new plain text part based on a template.
- *
- * @param text
- * The text to display
- * @return The part that displays the given text
- */
- private Part createPlainTextPart(String text) {
- return new TemplatePart(templateContextFactory, TemplateParser.parse(new StringReader("<% text|html>"))).set("text", text);
- }
-
- /**
- * Creates a new part based on a template that links to a site within the
- * normal internet.
- *
- * @param link
- * The target of the link
- * @param name
- * The name of the link
- * @return The part that displays the link
- */
- private Part createInternetLinkPart(String link, String name) {
- return new TemplatePart(templateContextFactory, TemplateParser.parse(new StringReader("<a class=\"internet\" href=\"/<% link|html>\" title=\"<% link|html>\"><% name|html></a>"))).set("link", link).set("name", name);
- }
-
- /**
- * Creates a new part based on a template that links to a site within
- * freenet.
- *
- * @param link
- * The target of the link
- * @param name
- * The name of the link
- * @return The part that displays the link
- */
- private Part createFreenetLinkPart(String link, String name) {
- return new TemplatePart(templateContextFactory, TemplateParser.parse(new StringReader("<a class=\"freenet\" href=\"/<% link|html>\" title=\"<% link|html>\"><% name|html></a>"))).set("link", link).set("name", name);
- }
-
- /**
- * Creates a new part based on a template that links to a page in the
- * poster’s keyspace.
- *
- * @param link
- * The target of the link
- * @param name
- * The name of the link
- * @return The part that displays the link
- */
- private Part createTrustedFreenetLinkPart(String link, String name) {
- return new TemplatePart(templateContextFactory, TemplateParser.parse(new StringReader("<a class=\"freenet-trusted\" href=\"/<% link|html>\" title=\"<% link|html>\"><% name|html></a>"))).set("link", link).set("name", name);
- }
-
- /**
- * Creates a new part based on a template that links to a page in Sone.
- *
- * @param link
- * The target of the link
- * @param name
- * The name of the link
- * @return The part that displays the link
- */
- private Part createInSoneLinkPart(String link, String name) {
- return createInSoneLinkPart(link, name, name);
- }
-
- /**
- * Creates a new part based on a template that links to a page in Sone.
- *
- * @param link
- * The target of the link
- * @param name
- * The name of the link
- * @param title
- * The title attribute of the link
- * @return The part that displays the link
- */
- private Part createInSoneLinkPart(String link, String name, String title) {
- return new TemplatePart(templateContextFactory, TemplateParser.parse(new StringReader("<a class=\"in-sone\" href=\"<%link|html>\" title=\"<%title|html>\"><%name|html></a>"))).set("link", link).set("name", name).set("title", title);
- }
-
-}
+++ /dev/null
-/*
- * Sone - FreenetLinkParserContext.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.text;
-
-import net.pterodactylus.sone.data.Sone;
-
-/**
- * {@link ParserContext} implementation for the {@link FreenetLinkParser}. It
- * stores the {@link Sone} that provided the parsed text so that certain links
- * can be marked in a different way.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class FreenetLinkParserContext implements ParserContext {
-
- /** The posting Sone. */
- private final Sone postingSone;
-
- /**
- * Creates a new link parser context.
- *
- * @param postingSone
- * The posting Sone
- */
- public FreenetLinkParserContext(Sone postingSone) {
- this.postingSone = postingSone;
- }
-
- /**
- * Returns the Sone that provided the text that is being parsed.
- *
- * @return The posting Sone
- */
- public Sone getPostingSone() {
- return postingSone;
- }
-
-}
--- /dev/null
+/*
+ * Sone - FreenetLinkPart.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.text;
+
+/**
+ * {@link LinkPart} implementation that stores an additional attribute: if the
+ * link is an SSK or USK link and the post was created by an identity that owns
+ * the keyspace in question.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FreenetLinkPart extends LinkPart {
+
+ /** Whether the link is trusted. */
+ private final boolean trusted;
+
+ /**
+ * Creates a new freenet link part.
+ *
+ * @param link
+ * The link of the part
+ * @param text
+ * The text of the part
+ * @param trusted
+ * {@code true} if the link is trusted, {@code false} otherwise
+ */
+ public FreenetLinkPart(String link, String text, boolean trusted) {
+ this(link, text, text, trusted);
+ }
+
+ /**
+ * Creates a new freenet link part.
+ *
+ * @param link
+ * The link of the part
+ * @param text
+ * The text of the part
+ * @param title
+ * The title of the part
+ * @param trusted
+ * {@code true} if the link is trusted, {@code false} otherwise
+ */
+ public FreenetLinkPart(String link, String text, String title, boolean trusted) {
+ super(link, text, title);
+ this.trusted = trusted;
+ }
+
+ //
+ // ACCESSORS
+ //
+
+ /**
+ * Returns whether the link is trusted.
+ *
+ * @return {@code true} if the link is trusted, {@code false} otherwise
+ */
+ public boolean isTrusted() {
+ return trusted;
+ }
+
+}
--- /dev/null
+/*
+ * Sone - LinkPart.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.text;
+
+/**
+ * {@link Part} implementation that can hold a link. A link contains of three
+ * attributes: the link itself, the text that is shown instead of the link, and
+ * an explanatory text that can be displayed e.g. as a tooltip.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class LinkPart implements Part {
+
+ /** The link of this part. */
+ private final String link;
+
+ /** The text of this part. */
+ private final String text;
+
+ /** The title of this part. */
+ private final String title;
+
+ /**
+ * Creates a new link part.
+ *
+ * @param link
+ * The link of the link part
+ * @param text
+ * The text of the link part
+ */
+ public LinkPart(String link, String text) {
+ this(link, text, text);
+ }
+
+ /**
+ * Creates a new link part.
+ *
+ * @param link
+ * The link of the link part
+ * @param text
+ * The text of the link part
+ * @param title
+ * The title of the link part
+ */
+ public LinkPart(String link, String text, String title) {
+ this.link = link;
+ this.text = text;
+ this.title = title;
+ }
+
+ //
+ // ACCESSORS
+ //
+
+ /**
+ * Returns the link of this part.
+ *
+ * @return The link of this part
+ */
+ public String getLink() {
+ return link;
+ }
+
+ /**
+ * Returns the text of this part.
+ *
+ * @return The text of this part
+ */
+ public String getText() {
+ return text;
+ }
+
+ /**
+ * Returns the title of this part.
+ *
+ * @return The title of this part
+ */
+ public String getTitle() {
+ return title;
+ }
+
+}
public interface Parser<C extends ParserContext> {
/**
- * Create a {@link Part} from the given text source.
+ * Create one or more {@link Part}s from the given text source.
*
* @param context
- * The parser context
+ * The parser context (may be {@code null})
* @param source
* The text source
- * @return The parsed part
+ * @return The parsed parts
* @throws IOException
* if an I/O error occurs
*/
- public Part parse(C context, Reader source) throws IOException;
+ public Iterable<Part> parse(C context, Reader source) throws IOException;
}
+/*
+ * Sone - ParserContext.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
+ * 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 <http://www.gnu.org/licenses/>.
+ */
package net.pterodactylus.sone.text;
package net.pterodactylus.sone.text;
-import net.pterodactylus.util.io.Renderable;
-
/**
* A part is a single piece of information that can be displayed as a single
- * element.
+ * element. How the part is displayed is not part of the {@code Part}
+ * specification.
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
-public interface Part extends Renderable {
+public interface Part {
- /* all required methods are inherited from {@link Renderable}. */
+ /* no methods. */
}
package net.pterodactylus.sone.text;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.io.Writer;
+import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Deque;
+import java.util.Iterator;
import java.util.List;
+import java.util.NoSuchElementException;
/**
* Part implementation that can contain an arbitrary amount of other parts.
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
-public class PartContainer implements Part {
+public class PartContainer implements Part, Iterable<Part> {
/** The parts to render. */
private final List<Part> parts = new ArrayList<Part>();
}
//
- // PART METHODS
+ // ITERABLE METHODS
//
/**
* {@inheritDoc}
*/
@Override
- public void render(Writer writer) throws IOException {
- for (Part part : parts) {
- part.render(writer);
- }
- }
+ @SuppressWarnings("synthetic-access")
+ public Iterator<Part> iterator() {
+ return new Iterator<Part>() {
- //
- // OBJECT METHODS
- //
+ private Deque<Iterator<Part>> partStack = new ArrayDeque<Iterator<Part>>();
+ private Part nextPart;
+ private boolean foundNextPart;
+ private boolean noNextPart;
- /**
- * {@inheritDoc}
- */
- @Override
- public String toString() {
- StringWriter stringWriter = new StringWriter();
- try {
- render(stringWriter);
- } catch (IOException ioe1) {
- /* should never throw, ignore. */
- }
- return stringWriter.toString();
+ {
+ partStack.push(parts.iterator());
+ }
+
+ private void findNext() {
+ if (foundNextPart) {
+ return;
+ }
+ noNextPart = true;
+ while (!partStack.isEmpty()) {
+ @SuppressWarnings("hiding")
+ Iterator<Part> parts = partStack.pop();
+ if (parts.hasNext()) {
+ nextPart = parts.next();
+ partStack.push(parts);
+ if (nextPart instanceof PartContainer) {
+ partStack.push(((PartContainer) nextPart).iterator());
+ } else {
+ noNextPart = false;
+ break;
+ }
+ }
+ }
+ foundNextPart = true;
+ }
+
+ @Override
+ public boolean hasNext() {
+ findNext();
+ return !noNextPart;
+ }
+
+ @Override
+ public Part next() {
+ findNext();
+ if (noNextPart) {
+ throw new NoSuchElementException();
+ }
+ foundNextPart = false;
+ return nextPart;
+ }
+
+ @Override
+ public void remove() {
+ /* ignore. */
+ }
+
+ };
}
}
--- /dev/null
+/*
+ * Sone - PlainTextPart.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.text;
+
+/**
+ * {@link Part} implementation that holds a single piece of text.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class PlainTextPart implements Part {
+
+ /** The text of the part. */
+ private final String text;
+
+ /**
+ * Creates a new plain-text part.
+ *
+ * @param text
+ * The text of the part
+ */
+ public PlainTextPart(String text) {
+ this.text = text;
+ }
+
+ //
+ // ACCESSORS
+ //
+
+ /**
+ * Returns the text of this part.
+ *
+ * @return The text of this part
+ */
+ public String getText() {
+ return text;
+ }
+
+}
--- /dev/null
+/*
+ * Sone - PostLinkPart.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.text;
+
+import net.pterodactylus.sone.data.Post;
+
+/**
+ * {@link Part} implementation that stores a reference to a {@link Post}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class PostPart implements Part {
+
+ /** The post this part refers to. */
+ private final Post post;
+
+ /**
+ * Creates a new post part.
+ *
+ * @param post
+ * The referenced post
+ */
+ public PostPart(Post post) {
+ this.post = post;
+ }
+
+ //
+ // ACCESSORS
+ //
+
+ /**
+ * Returns the post referenced by this part.
+ *
+ * @return The post referenced by this part
+ */
+ public Post getPost() {
+ return post;
+ }
+
+}
--- /dev/null
+/*
+ * Sone - SoneLinkPart.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.text;
+
+import net.pterodactylus.sone.data.Sone;
+
+/**
+ * {@link Part} implementation that stores a reference to a {@link Sone}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SonePart implements Part {
+
+ /** The referenced {@link Sone}. */
+ private final Sone sone;
+
+ /**
+ * Creates a new Sone part.
+ *
+ * @param sone
+ * The referenced Sone
+ */
+ public SonePart(Sone sone) {
+ this.sone = sone;
+ }
+
+ //
+ // ACCESSORS
+ //
+
+ /**
+ * Returns the referenced Sone.
+ *
+ * @return The referenced Sone
+ */
+ public Sone getSone() {
+ return sone;
+ }
+
+}
--- /dev/null
+/*
+ * Sone - FreenetLinkParser.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
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.text;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.net.MalformedURLException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import net.pterodactylus.sone.core.PostProvider;
+import net.pterodactylus.sone.core.SoneProvider;
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.util.logging.Logging;
+import freenet.keys.FreenetURI;
+
+/**
+ * {@link Parser} implementation that can recognize Freenet URIs.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneTextParser implements Parser<SoneTextParserContext> {
+
+ /** The logger. */
+ private static final Logger logger = Logging.getLogger(SoneTextParser.class);
+
+ /** Pattern to detect whitespace. */
+ private static final Pattern whitespacePattern = Pattern.compile("[\\u000a\u0020\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u200c\u200d\u202f\u205f\u2060\u2800\u3000]");
+
+ /**
+ * Enumeration for all recognized link types.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ private enum LinkType {
+
+ /** Link is a KSK. */
+ KSK,
+
+ /** Link is a CHK. */
+ CHK,
+
+ /** Link is an SSK. */
+ SSK,
+
+ /** Link is a USK. */
+ USK,
+
+ /** Link is HTTP. */
+ HTTP,
+
+ /** Link is HTTPS. */
+ HTTPS,
+
+ /** Link is a Sone. */
+ SONE,
+
+ /** Link is a post. */
+ POST,
+
+ }
+
+ /** The Sone provider. */
+ private final SoneProvider soneProvider;
+
+ /** The post provider. */
+ private final PostProvider postProvider;
+
+ /**
+ * Creates a new freenet link parser.
+ *
+ * @param soneProvider
+ * The Sone provider
+ * @param postProvider
+ * The post provider
+ */
+ public SoneTextParser(SoneProvider soneProvider, PostProvider postProvider) {
+ this.soneProvider = soneProvider;
+ this.postProvider = postProvider;
+ }
+
+ //
+ // PART METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Iterable<Part> parse(SoneTextParserContext context, Reader source) throws IOException {
+ PartContainer parts = new PartContainer();
+ BufferedReader bufferedReader = (source instanceof BufferedReader) ? (BufferedReader) source : new BufferedReader(source);
+ String line;
+ boolean lastLineEmpty = true;
+ int emptyLines = 0;
+ while ((line = bufferedReader.readLine()) != null) {
+ if (line.trim().length() == 0) {
+ if (lastLineEmpty) {
+ continue;
+ }
+ parts.add(new PlainTextPart("\n"));
+ ++emptyLines;
+ lastLineEmpty = emptyLines == 2;
+ continue;
+ }
+ emptyLines = 0;
+ /*
+ * lineComplete tracks whether the block you are parsing is the
+ * first block of the line. this is important because sometimes you
+ * have to add an additional line break.
+ */
+ boolean lineComplete = true;
+ while (line.length() > 0) {
+ int nextKsk = line.indexOf("KSK@");
+ int nextChk = line.indexOf("CHK@");
+ int nextSsk = line.indexOf("SSK@");
+ int nextUsk = line.indexOf("USK@");
+ int nextHttp = line.indexOf("http://");
+ int nextHttps = line.indexOf("https://");
+ int nextSone = line.indexOf("sone://");
+ int nextPost = line.indexOf("post://");
+ if ((nextKsk == -1) && (nextChk == -1) && (nextSsk == -1) && (nextUsk == -1) && (nextHttp == -1) && (nextHttps == -1) && (nextSone == -1) && (nextPost == -1)) {
+ if (lineComplete && !lastLineEmpty) {
+ parts.add(new PlainTextPart("\n" + line));
+ } else {
+ parts.add(new PlainTextPart(line));
+ }
+ break;
+ }
+ int next = Integer.MAX_VALUE;
+ LinkType linkType = null;
+ if ((nextKsk > -1) && (nextKsk < next)) {
+ next = nextKsk;
+ linkType = LinkType.KSK;
+ }
+ if ((nextChk > -1) && (nextChk < next)) {
+ next = nextChk;
+ linkType = LinkType.CHK;
+ }
+ if ((nextSsk > -1) && (nextSsk < next)) {
+ next = nextSsk;
+ linkType = LinkType.SSK;
+ }
+ if ((nextUsk > -1) && (nextUsk < next)) {
+ next = nextUsk;
+ linkType = LinkType.USK;
+ }
+ if ((nextHttp > -1) && (nextHttp < next)) {
+ next = nextHttp;
+ linkType = LinkType.HTTP;
+ }
+ if ((nextHttps > -1) && (nextHttps < next)) {
+ next = nextHttps;
+ linkType = LinkType.HTTPS;
+ }
+ if ((nextSone > -1) && (nextSone < next)) {
+ next = nextSone;
+ linkType = LinkType.SONE;
+ }
+ if ((nextPost > -1) && (nextPost < next)) {
+ next = nextPost;
+ linkType = LinkType.POST;
+ }
+
+ /* cut off “freenet:” from before keys. */
+ if (((linkType == LinkType.KSK) || (linkType == LinkType.CHK) || (linkType == LinkType.SSK) || (linkType == LinkType.USK)) && (next >= 8) && (line.substring(next - 8, next).equals("freenet:"))) {
+ next -= 8;
+ line = line.substring(0, next) + line.substring(next + 8);
+ }
+
+ /* if there is text before the next item, write it out. */
+ if (lineComplete && !lastLineEmpty) {
+ parts.add(new PlainTextPart("\n"));
+ }
+ if (next > 0) {
+ parts.add(new PlainTextPart(line.substring(0, next)));
+ line = line.substring(next);
+ next = 0;
+ }
+ lineComplete = false;
+
+ if (linkType == LinkType.SONE) {
+ if (line.length() >= (7 + 43)) {
+ String soneId = line.substring(7, 50);
+ Sone sone = soneProvider.getSone(soneId, false);
+ if ((sone != null) && (sone.getName() != null)) {
+ parts.add(new SonePart(sone));
+ } else {
+ parts.add(new PlainTextPart(line.substring(0, 50)));
+ }
+ line = line.substring(50);
+ } else {
+ parts.add(new PlainTextPart(line));
+ line = "";
+ }
+ continue;
+ }
+ if (linkType == LinkType.POST) {
+ if (line.length() >= (7 + 36)) {
+ String postId = line.substring(7, 43);
+ Post post = postProvider.getPost(postId, false);
+ if ((post != null) && (post.getSone() != null)) {
+ parts.add(new PostPart(post));
+ } else {
+ parts.add(new PlainTextPart(line.substring(0, 43)));
+ }
+ line = line.substring(43);
+ } else {
+ parts.add(new PlainTextPart(line));
+ line = "";
+ }
+ continue;
+ }
+ Matcher matcher = whitespacePattern.matcher(line);
+ int nextSpace = matcher.find(0) ? matcher.start() : line.length();
+ String link = line.substring(0, nextSpace);
+ String name = link;
+ logger.log(Level.FINER, "Found link: %s", link);
+ logger.log(Level.FINEST, "CHK: %d, SSK: %d, USK: %d", new Object[] { nextChk, nextSsk, nextUsk });
+
+ if ((linkType == LinkType.KSK) || (linkType == LinkType.CHK) || (linkType == LinkType.SSK) || (linkType == LinkType.USK)) {
+ FreenetURI uri;
+ if (name.indexOf('?') > -1) {
+ name = name.substring(0, name.indexOf('?'));
+ }
+ if (name.endsWith("/")) {
+ name = name.substring(0, name.length() - 1);
+ }
+ try {
+ uri = new FreenetURI(name);
+ name = uri.lastMetaString();
+ if (name == null) {
+ name = uri.getDocName();
+ }
+ if (name == null) {
+ name = link.substring(0, Math.min(9, link.length()));
+ }
+ boolean fromPostingSone = ((linkType == LinkType.SSK) || (linkType == LinkType.USK)) && (context != null) && (context.getPostingSone() != null) && link.substring(4, Math.min(link.length(), 47)).equals(context.getPostingSone().getId());
+ parts.add(new FreenetLinkPart(link, name, fromPostingSone));
+ } catch (MalformedURLException mue1) {
+ /* not a valid link, insert as plain text. */
+ parts.add(new PlainTextPart(link));
+ } catch (NullPointerException npe1) {
+ /* FreenetURI sometimes throws these, too. */
+ parts.add(new PlainTextPart(link));
+ } catch (ArrayIndexOutOfBoundsException aioobe1) {
+ /* oh, and these, too. */
+ parts.add(new PlainTextPart(link));
+ }
+ } else if ((linkType == LinkType.HTTP) || (linkType == LinkType.HTTPS)) {
+ name = link.substring(linkType == LinkType.HTTP ? 7 : 8);
+ int firstSlash = name.indexOf('/');
+ int lastSlash = name.lastIndexOf('/');
+ if ((lastSlash - firstSlash) > 3) {
+ name = name.substring(0, firstSlash + 1) + "…" + name.substring(lastSlash);
+ }
+ if (name.endsWith("/")) {
+ name = name.substring(0, name.length() - 1);
+ }
+ if (((name.indexOf('/') > -1) && (name.indexOf('.') < name.lastIndexOf('.', name.indexOf('/'))) || ((name.indexOf('/') == -1) && (name.indexOf('.') < name.lastIndexOf('.')))) && name.startsWith("www.")) {
+ name = name.substring(4);
+ }
+ if (name.indexOf('?') > -1) {
+ name = name.substring(0, name.indexOf('?'));
+ }
+ parts.add(new LinkPart(link, name));
+ }
+ line = line.substring(nextSpace);
+ }
+ lastLineEmpty = false;
+ }
+ for (int partIndex = parts.size() - 1; partIndex >= 0; --partIndex) {
+ Part part = parts.getPart(partIndex);
+ if (!(part instanceof PlainTextPart) || !"\n".equals(((PlainTextPart) part).getText())) {
+ break;
+ }
+ parts.removePart(partIndex);
+ }
+ return parts;
+ }
+
+}
--- /dev/null
+/*
+ * Sone - SoneTextParserContext.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.text;
+
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+
+/**
+ * {@link ParserContext} implementation for the {@link SoneTextParser}. It
+ * stores the {@link Sone} that provided the parsed text so that certain links
+ * can be marked in a different way.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneTextParserContext implements ParserContext {
+
+ /** The request being processed. */
+ private final FreenetRequest request;
+
+ /** The posting Sone. */
+ private final Sone postingSone;
+
+ /**
+ * Creates a new link parser context.
+ *
+ * @param request
+ * The request being processed
+ * @param postingSone
+ * The posting Sone
+ */
+ public SoneTextParserContext(FreenetRequest request, Sone postingSone) {
+ this.request = request;
+ this.postingSone = postingSone;
+ }
+
+ /**
+ * Returns the request that is currently being processed.
+ *
+ * @return The request being processed
+ */
+ public FreenetRequest getRequest() {
+ return request;
+ }
+
+ /**
+ * Returns the Sone that provided the text that is being parsed.
+ *
+ * @return The posting Sone
+ */
+ public Sone getPostingSone() {
+ return postingSone;
+ }
+
+}
+++ /dev/null
-/*
- * Sone - TemplatePart.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
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.text;
-
-import java.io.IOException;
-import java.io.StringWriter;
-import java.io.Writer;
-
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.template.TemplateContextFactory;
-import net.pterodactylus.util.template.TemplateException;
-
-/**
- * {@link Part} implementation that is rendered using a {@link Template}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class TemplatePart implements Part, net.pterodactylus.util.template.Part {
-
- /** The template context factory. */
- private final TemplateContextFactory templateContextFactory;
-
- /** The template to render for this part. */
- private final Template template;
-
- /**
- * Creates a new template part.
- *
- * @param templateContextFactory
- * The template context factory
- * @param template
- * The template to render
- */
- public TemplatePart(TemplateContextFactory templateContextFactory, Template template) {
- this.templateContextFactory = templateContextFactory;
- this.template = template;
- }
-
- //
- // ACTIONS
- //
-
- /**
- * Sets a variable in the template.
- *
- * @param key
- * The key of the variable
- * @param value
- * The value of the variable
- * @return This template part (for method chaining)
- */
- public TemplatePart set(String key, Object value) {
- template.getInitialContext().set(key, value);
- return this;
- }
-
- //
- // PART METHODS
- //
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void render(Writer writer) throws IOException {
- template.render(templateContextFactory.createTemplateContext().mergeContext(template.getInitialContext()), writer);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void render(TemplateContext templateContext, Writer writer) throws TemplateException {
- template.render(templateContext.mergeContext(template.getInitialContext()), writer);
- }
-
- //
- // OBJECT METHODS
- //
-
- /**
- * {@inheritDoc}
- */
- @Override
- public String toString() {
- StringWriter stringWriter = new StringWriter();
- try {
- render(stringWriter);
- } catch (IOException ioe1) {
- /* should never throw, ignore. */
- }
- return stringWriter.toString();
- }
-
-}
--- /dev/null
+/*
+ * Sone - TextFilter.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.text;
+
+/**
+ * Filter for newly inserted text. This filter strips HTTP links to the local
+ * node of identifying marks, e.g. a link to “http://localhost:8888/KSK@gpl.txt”
+ * will be converted to “KSK@gpl.txt”. This will only work for links that point
+ * to the same address Sone is accessed by, so if you access Sone using
+ * localhost:8888, links to 127.0.0.1:8888 will not be removed.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TextFilter {
+
+ /**
+ * Filters the given text, stripping the host header part for links to the
+ * local node.
+ *
+ * @param hostHeader
+ * The host header from the request
+ * @param text
+ * The text to filter
+ * @return The filtered text
+ */
+ public static String filter(String hostHeader, String text) {
+
+ /* filter http(s) links to own node. */
+ if (hostHeader != null) {
+ String line = text;
+ for (String toRemove : new String[] { "http://" + hostHeader + "/", "https://" + hostHeader + "/", "http://" + hostHeader, "https://" + hostHeader }) {
+ while (line.indexOf(toRemove) != -1) {
+ line = line.replace(toRemove, "");
+ }
+ }
+ return line;
+ }
+
+ /* not modified. */
+ return text;
+ }
+
+}
package net.pterodactylus.sone.web;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
import net.pterodactylus.util.version.Version;
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
templateContext.set("version", version);
}
package net.pterodactylus.sone.web;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* Page that lets the user bookmark a post.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.POST) {
String id = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
import java.util.Set;
import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.collection.Pagination;
import net.pterodactylus.util.filter.Filter;
import net.pterodactylus.util.filter.Filters;
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
Set<Post> allPosts = webInterface.getCore().getBookmarkedPosts();
Set<Post> loadedPosts = Filters.filteredSet(allPosts, new Filter<Post>() {
import net.pterodactylus.sone.data.Album;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* Page that lets the user create a new album.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.POST) {
String name = request.getHttpRequest().getPartAsStringFailsafe("name", 64).trim();
Album parent = webInterface.getCore().getAlbum(parentId, false);
Album album = webInterface.getCore().createAlbum(currentSone, parent);
album.setTitle(name).setDescription(description);
- webInterface.getCore().saveSone(currentSone);
+ webInterface.getCore().touchConfiguration();
throw new RedirectException("imageBrowser.html?album=" + album.getId());
}
}
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.text.TextFilter;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* This page lets the user create a new {@link Post}.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
if (request.getMethod() == Method.POST) {
sender = currentSone;
}
Sone recipient = webInterface.getCore().getSone(recipientId, false);
+ text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
webInterface.getCore().createPost(sender, recipient, System.currentTimeMillis(), text);
throw new RedirectException(returnPage);
}
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.text.TextFilter;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* This page lets the user post a reply to a post.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
String postId = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
String text = request.getHttpRequest().getPartAsStringFailsafe("text", 65536).trim();
if (sender == null) {
sender = getCurrentSone(request.getToadletContext());
}
+ text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
webInterface.getCore().createReply(sender, post, text);
throw new RedirectException(returnPage);
}
/*
- * FreenetSone - CreateSonePage.java - Copyright © 2010 David Roden
+ * Sone - CreateSonePage.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
import net.pterodactylus.sone.core.Core;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.logging.Logging;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
import freenet.clients.http.ToadletContext;
/**
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
List<OwnIdentity> ownIdentitiesWithoutSone = getOwnIdentitiesWithoutSone(webInterface.getCore());
templateContext.set("identitiesWithoutSone", ownIdentitiesWithoutSone);
*/
@Override
public boolean isEnabled(ToadletContext toadletContext) {
+ if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+ return false;
+ }
return (getCurrentSone(toadletContext, false) == null) || (webInterface.getCore().getLocalSones().size() == 1);
}
package net.pterodactylus.sone.web;
import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* Page that lets the user delete an {@link Album}.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.POST) {
String albumId = request.getHttpRequest().getPartAsStringFailsafe("album", 36);
package net.pterodactylus.sone.web;
import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* Page that lets the user delete an {@link Image}.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
String imageId = (request.getMethod() == Method.POST) ? request.getHttpRequest().getPartAsStringFailsafe("image", 36) : request.getHttpRequest().getParam("image");
Image image = webInterface.getCore().getImage(imageId, false);
package net.pterodactylus.sone.web;
import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* Lets the user delete a post they made.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.GET) {
String postId = request.getHttpRequest().getParam("post");
import net.pterodactylus.sone.data.Profile;
import net.pterodactylus.sone.data.Profile.Field;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* Page that lets the user confirm the deletion of a profile field.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
Sone currentSone = getCurrentSone(request.getToadletContext());
Profile profile = currentSone.getProfile();
package net.pterodactylus.sone.web;
import net.pterodactylus.sone.data.Reply;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* This page lets the user delete a reply.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
String replyId = request.getHttpRequest().getPartAsStringFailsafe("reply", 36);
Reply reply = webInterface.getCore().getReply(replyId);
/*
- * FreenetSone - DeleteSonePage.java - Copyright © 2010 David Roden
+ * Sone - DeleteSonePage.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
package net.pterodactylus.sone.web;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* Lets the user delete a Sone. Of course the Sone is not really deleted from
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.POST) {
if (request.getHttpRequest().isPartSet("deleteSone")) {
package net.pterodactylus.sone.web;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.notify.Notification;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
String notificationId = request.getHttpRequest().getPartAsStringFailsafe("notification", 36);
Notification notification = webInterface.getNotifications().getNotification(notificationId);
/*
- * Sone - TrustPage.java - Copyright © 2011 David Roden
+ * Sone - DistrustPage.java - Copyright © 2011 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
import net.pterodactylus.sone.core.Core;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* Page that lets the user distrust another Sone. This will assign a
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.POST) {
String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
package net.pterodactylus.sone.web;
import net.pterodactylus.sone.data.Album;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* TODO
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.POST) {
String albumId = request.getHttpRequest().getPartAsStringFailsafe("album", 36);
}
String description = request.getHttpRequest().getPartAsStringFailsafe("description", 1000).trim();
album.setTitle(title).setDescription(description);
- webInterface.getCore().saveSone(album.getSone());
+ webInterface.getCore().touchConfiguration();
throw new RedirectException("imageBrowser.html?album=" + album.getId());
}
}
package net.pterodactylus.sone.web;
import net.pterodactylus.sone.data.Image;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* Page that lets the user edit title and description of an {@link Image}.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.POST) {
String imageId = request.getHttpRequest().getPartAsStringFailsafe("image", 36);
}
image.setTitle(title);
image.setDescription(description);
- webInterface.getCore().saveSone(image.getSone());
+ webInterface.getCore().touchConfiguration();
throw new RedirectException("imageBrowser.html?image=" + image.getId());
}
}
import net.pterodactylus.sone.data.Profile;
import net.pterodactylus.sone.data.Profile.Field;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* Page that lets the user edit the name of a profile field.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
Sone currentSone = getCurrentSone(request.getToadletContext());
Profile profile = currentSone.getProfile();
import net.pterodactylus.sone.data.Profile;
import net.pterodactylus.sone.data.Profile.Field;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.number.Numbers;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
import freenet.clients.http.ToadletContext;
/**
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
ToadletContext toadletContenxt = request.getToadletContext();
Sone currentSone = getCurrentSone(toadletContenxt);
field.setValue(value);
}
currentSone.setProfile(profile);
- webInterface.getCore().saveSone(currentSone);
+ webInterface.getCore().touchConfiguration();
throw new RedirectException("editProfile.html");
} else if (request.getHttpRequest().getPartAsStringFailsafe("add-field", 4).equals("true")) {
String fieldName = request.getHttpRequest().getPartAsStringFailsafe("field-name", 256).trim();
profile.addField(fieldName);
currentSone.setProfile(profile);
fields = profile.getFields();
- webInterface.getCore().saveSone(currentSone);
+ webInterface.getCore().touchConfiguration();
throw new RedirectException("editProfile.html#profile-fields");
} catch (IllegalArgumentException iae1) {
templateContext.set("fieldName", fieldName);
* @return The parsed ID, or {@code null} if there was no part matching the
* given string
*/
- private String getFieldId(Request request, String partNameStart) {
+ private String getFieldId(FreenetRequest request, String partNameStart) {
for (String partName : request.getHttpRequest().getParts()) {
if (partName.startsWith(partNameStart)) {
return partName.substring(partNameStart.length());
package net.pterodactylus.sone.web;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* This page lets the user follow another Sone.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.POST) {
- String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
Sone currentSone = getCurrentSone(request.getToadletContext());
- currentSone.addFriend(soneId);
- webInterface.getCore().saveSone(currentSone);
+ String soneIds = request.getHttpRequest().getPartAsStringFailsafe("sone", 1200);
+ for (String soneId : soneIds.split("[ ,]+")) {
+ currentSone.addFriend(soneId);
+ }
+ webInterface.getCore().touchConfiguration();
throw new RedirectException(returnPage);
}
}
package net.pterodactylus.sone.web;
+import java.io.IOException;
+
import net.pterodactylus.sone.data.TemporaryImage;
-import net.pterodactylus.sone.web.page.Page;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Response;
/**
* Page that delivers a {@link TemporaryImage} to the browser.
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
-public class GetImagePage implements Page {
+public class GetImagePage implements Page<FreenetRequest> {
/** The Sone web interface. */
private final WebInterface webInterface;
* {@inheritDoc}
*/
@Override
- public Response handleRequest(Request request) {
+ public boolean isPrefixPage() {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response handleRequest(FreenetRequest request, Response response) throws IOException {
String imageId = request.getHttpRequest().getParam("image");
TemporaryImage temporaryImage = webInterface.getCore().getTemporaryImage(imageId);
if (temporaryImage == null) {
- return new Response(404, "Not found.", "text/plain; charset=utf-8", "");
+ return response.setStatusCode(404).setStatusText("Not found.").setContentType("text/html; charset=utf-8");
}
- return new Response(200, "OK", temporaryImage.getMimeType(), temporaryImage.getImageData()).setHeader("Content-Disposition", "attachment; filename=" + temporaryImage.getId() + "." + temporaryImage.getMimeType().substring(temporaryImage.getMimeType().lastIndexOf("/") + 1));
+ String contentType= temporaryImage.getMimeType();
+ return response.setStatusCode(200).setStatusText("OK").setContentType(contentType).addHeader("Content-Disposition", "attachment; filename=" + temporaryImage.getId() + "." + contentType.substring(contentType.lastIndexOf('/') + 1)).write(temporaryImage.getImageData());
}
}
import net.pterodactylus.sone.data.Album;
import net.pterodactylus.sone.data.Image;
import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
String albumId = request.getHttpRequest().getParam("album", null);
if (albumId != null) {
/*
- * FreenetSone - IndexPage.java - Copyright © 2010 David Roden
+ * Sone - IndexPage.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
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.notify.ListNotificationFilters;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.filter.Filter;
import net.pterodactylus.util.filter.Filters;
import net.pterodactylus.util.number.Numbers;
import net.pterodactylus.util.template.Template;
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
- Sone currentSone = getCurrentSone(request.getToadletContext());
+ final Sone currentSone = getCurrentSone(request.getToadletContext());
List<Post> allPosts = new ArrayList<Post>();
allPosts.addAll(currentSone.getPosts());
for (String friendSoneId : currentSone.getFriends()) {
}
}
}
+ allPosts = Filters.filteredList(allPosts, new Filter<Post>() {
+
+ @Override
+ public boolean filterObject(Post post) {
+ return ListNotificationFilters.isPostVisible(currentSone, post);
+ }
+ });
allPosts = Filters.filteredList(allPosts, Post.FUTURE_POSTS_FILTER);
Collections.sort(allPosts, Post.TIME_COMPARATOR);
Pagination<Post> pagination = new Pagination<Post>(allPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
import java.util.List;
import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.collection.ReverseComparator;
+import net.pterodactylus.util.filter.Filter;
import net.pterodactylus.util.filter.Filters;
import net.pterodactylus.util.number.Numbers;
import net.pterodactylus.util.template.Template;
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
+ String sortField = request.getHttpRequest().getParam("sort");
+ String sortOrder = request.getHttpRequest().getParam("order");
+ String followedSones = request.getHttpRequest().getParam("followedSones");
+ templateContext.set("sort", (sortField != null) ? sortField : "name");
+ templateContext.set("order", (sortOrder != null) ? sortOrder : "asc");
+ templateContext.set("followedSones", followedSones);
+ final Sone currentSone = getCurrentSone(request.getToadletContext(), false);
List<Sone> knownSones = Filters.filteredList(new ArrayList<Sone>(webInterface.getCore().getSones()), Sone.EMPTY_SONE_FILTER);
- Collections.sort(knownSones, Sone.NICE_NAME_COMPARATOR);
+ if ((currentSone != null) && "show-only".equals(followedSones)) {
+ knownSones = Filters.filteredList(knownSones, new Filter<Sone>() {
+
+ @Override
+ public boolean filterObject(Sone sone) {
+ return currentSone.hasFriend(sone.getId());
+ }
+ });
+ } else if ((currentSone != null) && "hide".equals(followedSones)) {
+ knownSones = Filters.filteredList(knownSones, new Filter<Sone>() {
+
+ @Override
+ public boolean filterObject(Sone sone) {
+ return !currentSone.hasFriend(sone.getId());
+ }
+ });
+ }
+ if ("activity".equals(sortField)) {
+ if ("asc".equals(sortOrder)) {
+ Collections.sort(knownSones, new ReverseComparator<Sone>(Sone.LAST_ACTIVITY_COMPARATOR));
+ } else {
+ Collections.sort(knownSones, Sone.LAST_ACTIVITY_COMPARATOR);
+ }
+ } else if ("posts".equals(sortField)) {
+ if ("asc".equals(sortOrder)) {
+ Collections.sort(knownSones, new ReverseComparator<Sone>(Sone.POST_COUNT_COMPARATOR));
+ } else {
+ Collections.sort(knownSones, Sone.POST_COUNT_COMPARATOR);
+ }
+ } else {
+ if ("desc".equals(sortOrder)) {
+ Collections.sort(knownSones, new ReverseComparator<Sone>(Sone.NICE_NAME_COMPARATOR));
+ } else {
+ Collections.sort(knownSones, Sone.NICE_NAME_COMPARATOR);
+ }
+ }
Pagination<Sone> sonePagination = new Pagination<Sone>(knownSones, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
templateContext.set("pagination", sonePagination);
templateContext.set("knownSones", sonePagination.getItems());
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* Page that lets the user like a {@link Post}.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.POST) {
- String type=request.getHttpRequest().getPartAsStringFailsafe("type", 16);
+ String type = request.getHttpRequest().getPartAsStringFailsafe("type", 16);
String id = request.getHttpRequest().getPartAsStringFailsafe(type, 36);
String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
Sone currentSone = getCurrentSone(request.getToadletContext());
package net.pterodactylus.sone.web;
import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
Sone sone = webInterface.getCore().getLocalSone(soneId, false);
/*
- * FreenetSone - LoginPage.java - Copyright © 2010 David Roden
+ * Sone - LoginPage.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
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.freenet.wot.OwnIdentity;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.logging.Logging;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
import freenet.clients.http.ToadletContext;
/**
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
/* get all own identities. */
List<Sone> localSones = new ArrayList<Sone>(webInterface.getCore().getLocalSones());
* {@inheritDoc}
*/
@Override
- protected String getRedirectTarget(Request request) {
+ protected String getRedirectTarget(FreenetRequest request) {
if (getCurrentSone(request.getToadletContext(), false) != null) {
return "index.html";
}
*/
@Override
public boolean isEnabled(ToadletContext toadletContext) {
+ if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+ return false;
+ }
return getCurrentSone(toadletContext, false) == null;
}
/*
- * FreenetSone - LogoutPage.java - Copyright © 2010 David Roden
+ * Sone - LogoutPage.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
package net.pterodactylus.sone.web;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
import freenet.clients.http.ToadletContext;
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
setCurrentSone(request.getToadletContext(), null);
super.processTemplate(request, templateContext);
throw new RedirectException("index.html");
*/
@Override
public boolean isEnabled(ToadletContext toadletContext) {
+ if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+ return false;
+ }
return (getCurrentSone(toadletContext, false) != null) && (webInterface.getCore().getLocalSones().size() != 1);
}
/*
- * Sone - MarkReadPage.java - Copyright © 2011 David Roden
+ * Sone - MarkAsKnownPage.java - Copyright © 2011 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
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Reply;
import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
String type = request.getHttpRequest().getPartAsStringFailsafe("type", 5);
if (!type.equals("sone") && !type.equals("post") && !type.equals("reply")) {
package net.pterodactylus.sone.web;
+import java.util.ArrayList;
+import java.util.List;
+
import net.pterodactylus.sone.core.Core.Preferences;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.number.Numbers;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* This page lets the user edit the options of the Sone plugin.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
Preferences preferences = webInterface.getCore().getPreferences();
Sone currentSone = webInterface.getCurrentSone(request.getToadletContext(), false);
if (request.getMethod() == Method.POST) {
+ List<String> fieldErrors = new ArrayList<String>();
if (currentSone != null) {
boolean autoFollow = request.getHttpRequest().isPartSet("auto-follow");
currentSone.getOptions().getBooleanOption("AutoFollow").set(autoFollow);
- webInterface.getCore().saveSone(currentSone);
+ boolean enableSoneInsertNotifications = request.getHttpRequest().isPartSet("enable-sone-insert-notifications");
+ currentSone.getOptions().getBooleanOption("EnableSoneInsertNotifications").set(enableSoneInsertNotifications);
+ webInterface.getCore().touchConfiguration();
}
Integer insertionDelay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("insertion-delay", 16));
- preferences.setInsertionDelay(insertionDelay);
+ if (!preferences.validateInsertionDelay(insertionDelay)) {
+ fieldErrors.add("insertion-delay");
+ } else {
+ preferences.setInsertionDelay(insertionDelay);
+ }
Integer postsPerPage = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("posts-per-page", 4), null);
- preferences.setPostsPerPage(postsPerPage);
+ if (!preferences.validatePostsPerPage(postsPerPage)) {
+ fieldErrors.add("posts-per-page");
+ } else {
+ preferences.setPostsPerPage(postsPerPage);
+ }
+ Integer charactersPerPost = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("characters-per-post", 10), null);
+ if (!preferences.validateCharactersPerPost(charactersPerPost)) {
+ fieldErrors.add("characters-per-post");
+ } else {
+ preferences.setCharactersPerPost(charactersPerPost);
+ }
+ boolean requireFullAccess = request.getHttpRequest().isPartSet("require-full-access");
+ preferences.setRequireFullAccess(requireFullAccess);
Integer positiveTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("positive-trust", 3));
- preferences.setPositiveTrust(positiveTrust);
+ if (!preferences.validatePositiveTrust(positiveTrust)) {
+ fieldErrors.add("positive-trust");
+ } else {
+ preferences.setPositiveTrust(positiveTrust);
+ }
Integer negativeTrust = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("negative-trust", 4));
- preferences.setNegativeTrust(negativeTrust);
+ if (!preferences.validateNegativeTrust(negativeTrust)) {
+ fieldErrors.add("negative-trust");
+ } else {
+ preferences.setNegativeTrust(negativeTrust);
+ }
String trustComment = request.getHttpRequest().getPartAsStringFailsafe("trust-comment", 256);
if (trustComment.trim().length() == 0) {
trustComment = null;
}
preferences.setTrustComment(trustComment);
- boolean soneRescueMode = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("sone-rescue-mode", 5));
- preferences.setSoneRescueMode(soneRescueMode);
+ boolean fcpInterfaceActive = request.getHttpRequest().isPartSet("fcp-interface-active");
+ preferences.setFcpInterfaceActive(fcpInterfaceActive);
+ Integer fcpFullAccessRequiredInteger = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("fcp-full-access-required", 1), preferences.getFcpFullAccessRequired().ordinal());
+ FullAccessRequired fcpFullAccessRequired = FullAccessRequired.values()[fcpFullAccessRequiredInteger];
+ preferences.setFcpFullAccessRequired(fcpFullAccessRequired);
boolean clearOnNextRestart = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("clear-on-next-restart", 5));
preferences.setClearOnNextRestart(clearOnNextRestart);
boolean reallyClearOnNextRestart = Boolean.parseBoolean(request.getHttpRequest().getPartAsStringFailsafe("really-clear-on-next-restart", 5));
preferences.setReallyClearOnNextRestart(reallyClearOnNextRestart);
- webInterface.getCore().saveConfiguration();
- throw new RedirectException(getPath());
+ webInterface.getCore().touchConfiguration();
+ if (fieldErrors.isEmpty()) {
+ throw new RedirectException(getPath());
+ }
+ templateContext.set("fieldErrors", fieldErrors);
}
if (currentSone != null) {
templateContext.set("auto-follow", currentSone.getOptions().getBooleanOption("AutoFollow").get());
+ templateContext.set("enable-sone-insert-notifications", currentSone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get());
}
templateContext.set("insertion-delay", preferences.getInsertionDelay());
templateContext.set("posts-per-page", preferences.getPostsPerPage());
+ templateContext.set("characters-per-post", preferences.getCharactersPerPost());
+ templateContext.set("require-full-access", preferences.isRequireFullAccess());
templateContext.set("positive-trust", preferences.getPositiveTrust());
templateContext.set("negative-trust", preferences.getNegativeTrust());
templateContext.set("trust-comment", preferences.getTrustComment());
- templateContext.set("sone-rescue-mode", preferences.isSoneRescueMode());
+ templateContext.set("fcp-interface-active", preferences.isFcpInterfaceActive());
+ templateContext.set("fcp-full-access-required", preferences.getFcpFullAccessRequired().ordinal());
templateContext.set("clear-on-next-restart", preferences.isClearOnNextRestart());
templateContext.set("really-clear-on-next-restart", preferences.isReallyClearOnNextRestart());
}
--- /dev/null
+/*
+ * Sone - RescuePage.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.core.SoneRescuer;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user control the rescue mode for a Sone.
+ *
+ * @see SoneRescuer
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class RescuePage extends SoneTemplatePage {
+
+ /**
+ * Creates a new rescue page.
+ *
+ * @param template
+ * The template to render
+ * @param webInterface
+ * The Sone web interface
+ */
+ public RescuePage(Template template, WebInterface webInterface) {
+ super("rescue.html", template, "Page.Rescue.Title", webInterface, true);
+ }
+
+ //
+ // SONETEMPLATEPAGE METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+ super.processTemplate(request, templateContext);
+ Sone currentSone = getCurrentSone(request.getToadletContext(), false);
+ SoneRescuer soneRescuer = webInterface.getCore().getSoneRescuer(currentSone);
+ if (request.getMethod() == Method.POST) {
+ if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("fetch", 4))) {
+ long edition = Numbers.safeParseLong(request.getHttpRequest().getPartAsStringFailsafe("edition", 8), -1L);
+ if (edition > -1) {
+ soneRescuer.setEdition(edition);
+ }
+ soneRescuer.startNextFetch();
+ }
+ throw new RedirectException("rescue.html");
+ }
+ templateContext.set("soneRescuer", soneRescuer);
+ }
+
+}
/*
- * Sone - OptionsPage.java - Copyright © 2010 David Roden
+ * Sone - SearchPage.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
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import net.pterodactylus.sone.data.Post;
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.util.collection.Converter;
-import net.pterodactylus.util.collection.Converters;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.cache.Cache;
+import net.pterodactylus.util.cache.CacheException;
+import net.pterodactylus.util.cache.CacheItem;
+import net.pterodactylus.util.cache.DefaultCacheItem;
+import net.pterodactylus.util.cache.MemoryCache;
+import net.pterodactylus.util.cache.ValueRetriever;
+import net.pterodactylus.util.collection.Mapper;
+import net.pterodactylus.util.collection.Mappers;
import net.pterodactylus.util.collection.Pagination;
+import net.pterodactylus.util.collection.TimedMap;
import net.pterodactylus.util.filter.Filter;
import net.pterodactylus.util.filter.Filters;
+import net.pterodactylus.util.logging.Logging;
import net.pterodactylus.util.number.Numbers;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
*/
public class SearchPage extends SoneTemplatePage {
+ /** The logger. */
+ private static final Logger logger = Logging.getLogger(SearchPage.class);
+
+ /** Short-term cache. */
+ private final Cache<List<Phrase>, Set<Hit<Post>>> hitCache = new MemoryCache<List<Phrase>, Set<Hit<Post>>>(new ValueRetriever<List<Phrase>, Set<Hit<Post>>>() {
+
+ @SuppressWarnings("synthetic-access")
+ public CacheItem<Set<Hit<Post>>> retrieve(List<Phrase> phrases) throws CacheException {
+ Set<Post> posts = new HashSet<Post>();
+ for (Sone sone : webInterface.getCore().getSones()) {
+ posts.addAll(sone.getPosts());
+ }
+ return new DefaultCacheItem<Set<Hit<Post>>>(getHits(Filters.filteredSet(posts, Post.FUTURE_POSTS_FILTER), phrases, new PostStringGenerator()));
+ }
+
+ }, new TimedMap<List<Phrase>, CacheItem<Set<Hit<Post>>>>(300000));
+
/**
* Creates a new search page.
*
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
String query = request.getHttpRequest().getParam("query").trim();
if (query.length() == 0) {
}
List<Phrase> phrases = parseSearchPhrases(query);
+ if (phrases.isEmpty()) {
+ throw new RedirectException("index.html");
+ }
Set<Sone> sones = webInterface.getCore().getSones();
Set<Hit<Sone>> soneHits = getHits(sones, phrases, SoneStringGenerator.COMPLETE_GENERATOR);
- Set<Post> posts = new HashSet<Post>();
- for (Sone sone : sones) {
- posts.addAll(sone.getPosts());
+ Set<Hit<Post>> postHits;
+ try {
+ postHits = hitCache.get(phrases);
+ } catch (CacheException ce1) {
+ /* should never happen. */
+ logger.log(Level.SEVERE, "Could not get search results from cache!", ce1);
+ postHits = Collections.emptySet();
}
- @SuppressWarnings("synthetic-access")
- Set<Hit<Post>> postHits = getHits(Filters.filteredSet(posts, Post.FUTURE_POSTS_FILTER), phrases, new PostStringGenerator());
/* now filter. */
soneHits = Filters.filteredSet(soneHits, Hit.POSITIVE_FILTER);
Collections.sort(sortedPostHits, Hit.DESCENDING_COMPARATOR);
/* extract Sones and posts. */
- List<Sone> resultSones = Converters.convertList(sortedSoneHits, new HitConverter<Sone>());
- List<Post> resultPosts = Converters.convertList(sortedPostHits, new HitConverter<Post>());
+ List<Sone> resultSones = Mappers.mappedList(sortedSoneHits, new HitMapper<Sone>());
+ List<Post> resultPosts = Mappers.mappedList(sortedPostHits, new HitMapper<Post>());
/* pagination. */
Pagination<Sone> sonePagination = new Pagination<Sone>(resultSones, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("sonePage"), 0));
Set<Hit<T>> hits = new HashSet<Hit<T>>();
for (T object : objects) {
String objectString = stringGenerator.generateString(object);
- int score = calculateScore(phrases, objectString);
+ double score = calculateScore(phrases, objectString);
hits.add(new Hit<T>(object, score));
}
return hits;
List<Phrase> phrases = new ArrayList<Phrase>();
for (String phrase : parsedPhrases) {
if (phrase.startsWith("+")) {
- phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.REQUIRED));
+ if (phrase.length() > 1) {
+ phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.REQUIRED));
+ } else {
+ phrases.add(new Phrase("+", Phrase.Optionality.OPTIONAL));
+ }
} else if (phrase.startsWith("-")) {
- phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.FORBIDDEN));
+ if (phrase.length() > 1) {
+ phrases.add(new Phrase(phrase.substring(1), Phrase.Optionality.FORBIDDEN));
+ } else {
+ phrases.add(new Phrase("-", Phrase.Optionality.OPTIONAL));
+ }
+ } else {
+ phrases.add(new Phrase(phrase, Phrase.Optionality.OPTIONAL));
}
- phrases.add(new Phrase(phrase, Phrase.Optionality.OPTIONAL));
}
return phrases;
}
* The expression to search
* @return The score of the expression
*/
- private int calculateScore(List<Phrase> phrases, String expression) {
- int optionalHits = 0;
- int requiredHits = 0;
+ private double calculateScore(List<Phrase> phrases, String expression) {
+ logger.log(Level.FINEST, "Calculating Score for “%s”…", expression);
+ double optionalHits = 0;
+ double requiredHits = 0;
int forbiddenHits = 0;
int requiredPhrases = 0;
for (Phrase phrase : phrases) {
}
int matches = 0;
int index = 0;
+ double score = 0;
while (index < expression.length()) {
int position = expression.toLowerCase().indexOf(phraseString, index);
if (position == -1) {
break;
}
+ score += Math.pow(1 - position / (double) expression.length(), 2);
index = position + phraseString.length();
+ logger.log(Level.FINEST, "Got hit at position %d.", position);
++matches;
}
+ logger.log(Level.FINEST, "Score: %f", score);
if (matches == 0) {
continue;
}
if (phrase.getOptionality() == Phrase.Optionality.REQUIRED) {
- requiredHits += matches;
+ requiredHits += score;
}
if (phrase.getOptionality() == Phrase.Optionality.OPTIONAL) {
- optionalHits += matches;
+ optionalHits += score;
}
if (phrase.getOptionality() == Phrase.Optionality.FORBIDDEN) {
forbiddenHits += matches;
return optionality;
}
+ //
+ // OBJECT METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return phrase.hashCode() ^ ((optionality == Optionality.FORBIDDEN) ? (0xaaaaaaaa) : ((optionality == Optionality.REQUIRED) ? 0x55555555 : 0));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof Phrase)) {
+ return false;
+ }
+ @SuppressWarnings("hiding")
+ Phrase phrase = (Phrase) object;
+ return (this.optionality == phrase.optionality) && this.phrase.equals(phrase.phrase);
+ }
+
}
/**
@Override
public int compare(Hit<?> leftHit, Hit<?> rightHit) {
- return rightHit.getScore() - leftHit.getScore();
+ return (rightHit.getScore() < leftHit.getScore()) ? -1 : ((rightHit.getScore() > leftHit.getScore()) ? 1 : 0);
}
};
private final T object;
/** The score of the object. */
- private final int score;
+ private final double score;
/**
* Creates a new hit.
* @param score
* The score of the object
*/
- public Hit(T object, int score) {
+ public Hit(T object, double score) {
this.object = object;
this.score = score;
}
*
* @return The score of the object
*/
- public int getScore() {
+ public double getScore() {
return score;
}
* The type of the object to extract
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
- public static class HitConverter<T> implements Converter<Hit<T>, T> {
+ public static class HitMapper<T> implements Mapper<Hit<T>, T> {
/**
* {@inheritDoc}
*/
@Override
- public T convert(Hit<T> input) {
+ public T map(Hit<T> input) {
return input.getObject();
}
/*
- * Freetalk - FreetalkTemplatePage.java - Copyright © 2010 David Roden
+ * Sone - SoneTemplatePage.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
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.main.SonePlugin;
-import net.pterodactylus.sone.web.page.Page;
+import net.pterodactylus.sone.notify.ListNotificationFilters;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.sone.web.page.FreenetTemplatePage;
import net.pterodactylus.util.collection.ListBuilder;
import net.pterodactylus.util.collection.MapBuilder;
import freenet.support.api.HTTPRequest;
/**
- * Base page for the Freetalk web interface.
+ * Base page for the Sone web interface.
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
private final boolean requireLogin;
/**
- * Creates a new template page for Freetalk that does not require the user
- * to be logged in.
+ * Creates a new template page for Sone that does not require the user to be
+ * logged in.
*
* @param path
* The path of the page
}
/**
- * Creates a new template page for Freetalk that does not require the user
- * to be logged in.
+ * Creates a new template page for Sone that does not require the user to be
+ * logged in.
*
* @param path
* The path of the page
}
/**
- * Creates a new template page for Freetalk.
+ * Creates a new template page for Sone.
*
* @param path
* The path of the page
}
/**
- * Creates a new template page for Freetalk.
+ * Creates a new template page for Sone.
*
* @param path
* The path of the page
this.pageTitleKey = pageTitleKey;
this.webInterface = webInterface;
this.requireLogin = requireLogin;
- template.getInitialContext().set("webInterface", webInterface);
}
//
* {@inheritDoc}
*/
@Override
- protected String getPageTitle(Request request) {
+ protected String getPageTitle(FreenetRequest request) {
if (pageTitleKey != null) {
return webInterface.getL10n().getString(pageTitleKey);
}
* {@inheritDoc}
*/
@Override
- protected List<Map<String, String>> getAdditionalLinkNodes(Request request) {
+ protected List<Map<String, String>> getAdditionalLinkNodes(FreenetRequest request) {
return new ListBuilder<Map<String, String>>().add(new MapBuilder<String, String>().put("rel", "search").put("type", "application/opensearchdescription+xml").put("title", "Sone").put("href", "http://" + request.getHttpRequest().getHeader("host") + "/Sone/OpenSearch.xml").get()).get();
}
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
- templateContext.set("currentSone", getCurrentSone(request.getToadletContext(), false));
+ Sone currentSone = getCurrentSone(request.getToadletContext(), false);
+ templateContext.set("core", webInterface.getCore());
+ templateContext.set("currentSone", currentSone);
templateContext.set("localSones", webInterface.getCore().getLocalSones());
templateContext.set("request", request);
templateContext.set("currentVersion", SonePlugin.VERSION);
templateContext.set("latestEdition", webInterface.getCore().getUpdateChecker().getLatestEdition());
templateContext.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion());
templateContext.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate());
+ templateContext.set("notifications", ListNotificationFilters.filterNotifications(webInterface.getNotifications().getNotifications(), currentSone));
}
/**
* {@inheritDoc}
*/
@Override
- protected String getRedirectTarget(Page.Request request) {
+ protected String getRedirectTarget(FreenetRequest request) {
if (requiresLogin() && (getCurrentSone(request.getToadletContext(), false) == null)) {
HTTPRequest httpRequest = request.getHttpRequest();
String originalUrl = httpRequest.getPath();
* {@inheritDoc}
*/
@Override
+ protected boolean isFullAccessOnly() {
+ return webInterface.getCore().getPreferences().isRequireFullAccess();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
public boolean isEnabled(ToadletContext toadletContext) {
+ if (webInterface.getCore().getPreferences().isRequireFullAccess() && !toadletContext.isAllowedFullAccess()) {
+ return false;
+ }
if (requiresLogin()) {
return getCurrentSone(toadletContext, false) != null;
}
import net.pterodactylus.sone.core.Core;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* Page that lets the user trust another Sone. This will assign a configurable
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.POST) {
String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
/*
- * Sone - BookmarkPage.java - Copyright © 2011 David Roden
+ * Sone - UnbookmarkPage.java - Copyright © 2011 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
import java.util.Set;
import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* Page that lets the user unbookmark a post.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.POST) {
String id = request.getHttpRequest().getPartAsStringFailsafe("post", 36);
/*
- * Sone - FollowSonePage.java - Copyright © 2010 David Roden
+ * Sone - UnfollowSonePage.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
package net.pterodactylus.sone.web;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* This page lets the user unfollow another Sone.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.POST) {
- String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
Sone currentSone = getCurrentSone(request.getToadletContext());
- currentSone.removeFriend(soneId);
- webInterface.getCore().saveSone(currentSone);
+ String soneIds = request.getHttpRequest().getPartAsStringFailsafe("sone", 2000);
+ for (String soneId : soneIds.split("[ ,]+")) {
+ currentSone.removeFriend(soneId);
+ }
+ webInterface.getCore().touchConfiguration();
throw new RedirectException(returnPage);
}
}
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* Page that lets the user unlike a {@link Post}.
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.POST) {
String type = request.getHttpRequest().getPartAsStringFailsafe("type", 16);
/*
- * Sone - LockSonePage.java - Copyright © 2010 David Roden
+ * Sone - UnlockSonePage.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
package net.pterodactylus.sone.web;
import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
String soneId = request.getHttpRequest().getPartAsStringFailsafe("sone", 44);
Sone sone = webInterface.getCore().getLocalSone(soneId, false);
/*
- * Sone - TrustPage.java - Copyright © 2011 David Roden
+ * Sone - UntrustPage.java - Copyright © 2011 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
import net.pterodactylus.sone.core.Core;
import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
/**
* Page that lets the user untrust another Sone. This will remove all trust
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.POST) {
String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
import net.pterodactylus.sone.data.Album;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.data.TemporaryImage;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.io.Closer;
import net.pterodactylus.util.io.StreamCopier;
import net.pterodactylus.util.logging.Logging;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
import freenet.support.api.Bucket;
import freenet.support.api.HTTPUploadedFile;
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
if (request.getMethod() == Method.POST) {
Sone currentSone = getCurrentSone(request.getToadletContext());
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.template.SoneAccessor;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
* {@inheritDoc}
*/
@Override
- protected String getPageTitle(Request request) {
+ protected String getPageTitle(FreenetRequest request) {
String postId = request.getHttpRequest().getParam("post");
Post post = webInterface.getCore().getPost(postId, false);
String title = "";
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
String postId = request.getHttpRequest().getParam("post");
boolean raw = request.getHttpRequest().getParam("raw").equals("true");
import net.pterodactylus.sone.data.Reply;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.template.SoneAccessor;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.collection.Pagination;
import net.pterodactylus.util.number.Numbers;
import net.pterodactylus.util.template.Template;
* {@inheritDoc}
*/
@Override
- protected String getPageTitle(Request request) {
+ protected String getPageTitle(FreenetRequest request) {
String soneId = request.getHttpRequest().getParam("sone");
Sone sone = webInterface.getCore().getSone(soneId, false);
if ((sone != null) && (sone.getTime() > 0)) {
* {@inheritDoc}
*/
@Override
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
super.processTemplate(request, templateContext);
String soneId = request.getHttpRequest().getParam("sone");
Sone sone = webInterface.getCore().getSone(soneId, false);
templateContext.set("sone", sone);
+ templateContext.set("soneId", soneId);
+ if (sone == null) {
+ return;
+ }
List<Post> sonePosts = sone.getPosts();
sonePosts.addAll(webInterface.getCore().getDirectedPosts(sone));
Collections.sort(sonePosts, Post.TIME_COMPARATOR);
/*
- * FreenetSone - WebInterface.java - Copyright © 2010 David Roden
+ * Sone - WebInterface.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
package net.pterodactylus.sone.web;
+import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import net.pterodactylus.sone.template.IdentityAccessor;
import net.pterodactylus.sone.template.ImageLinkFilter;
import net.pterodactylus.sone.template.JavascriptFilter;
-import net.pterodactylus.sone.template.NotificationManagerAccessor;
import net.pterodactylus.sone.template.ParserFilter;
import net.pterodactylus.sone.template.PostAccessor;
import net.pterodactylus.sone.template.ReplyAccessor;
import net.pterodactylus.sone.template.SoneAccessor;
import net.pterodactylus.sone.template.SubstringFilter;
import net.pterodactylus.sone.template.TrustAccessor;
+import net.pterodactylus.sone.template.UniqueElementFilter;
import net.pterodactylus.sone.template.UnknownDateFilter;
+import net.pterodactylus.sone.text.Part;
+import net.pterodactylus.sone.text.SonePart;
+import net.pterodactylus.sone.text.SoneTextParser;
import net.pterodactylus.sone.web.ajax.BookmarkAjaxPage;
import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage;
import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
+import net.pterodactylus.sone.web.ajax.GetNotificationAjaxPage;
import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
import net.pterodactylus.sone.web.ajax.GetReplyAjaxPage;
import net.pterodactylus.sone.web.ajax.GetStatusAjaxPage;
import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage;
import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
import net.pterodactylus.sone.web.ajax.UntrustAjaxPage;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.sone.web.page.PageToadlet;
import net.pterodactylus.sone.web.page.PageToadletFactory;
-import net.pterodactylus.sone.web.page.RedirectPage;
-import net.pterodactylus.sone.web.page.StaticPage;
-import net.pterodactylus.sone.web.page.TemplatePage;
import net.pterodactylus.util.cache.Cache;
import net.pterodactylus.util.cache.CacheException;
import net.pterodactylus.util.cache.CacheItem;
import net.pterodactylus.util.cache.DefaultCacheItem;
import net.pterodactylus.util.cache.MemoryCache;
import net.pterodactylus.util.cache.ValueRetriever;
+import net.pterodactylus.util.collection.SetBuilder;
+import net.pterodactylus.util.filter.Filters;
import net.pterodactylus.util.logging.Logging;
import net.pterodactylus.util.notify.Notification;
import net.pterodactylus.util.notify.NotificationManager;
import net.pterodactylus.util.notify.TemplateNotification;
import net.pterodactylus.util.template.CollectionSortFilter;
+import net.pterodactylus.util.template.ContainsFilter;
import net.pterodactylus.util.template.DateFilter;
import net.pterodactylus.util.template.FormatFilter;
import net.pterodactylus.util.template.HtmlFilter;
import net.pterodactylus.util.template.XmlFilter;
import net.pterodactylus.util.thread.Ticker;
import net.pterodactylus.util.version.Version;
+import net.pterodactylus.util.web.RedirectPage;
+import net.pterodactylus.util.web.StaticPage;
+import net.pterodactylus.util.web.TemplatePage;
import freenet.clients.http.SessionManager;
-import freenet.clients.http.SessionManager.Session;
import freenet.clients.http.ToadletContainer;
import freenet.clients.http.ToadletContext;
+import freenet.clients.http.SessionManager.Session;
import freenet.l10n.BaseL10n;
import freenet.support.api.HTTPRequest;
/** The template context factory. */
private final TemplateContextFactory templateContextFactory;
+ /** The Sone text parser. */
+ private final SoneTextParser soneTextParser;
+
/** The “new Sone” notification. */
private final ListNotification<Sone> newSoneNotification;
/** The “new reply” notification. */
private final ListNotification<Reply> newReplyNotification;
- /** The “rescuing Sone” notification. */
- private final ListNotification<Sone> rescuingSonesNotification;
+ /** The invisible “local post” notification. */
+ private final ListNotification<Post> localPostNotification;
+
+ /** The invisible “local reply” notification. */
+ private final ListNotification<Reply> localReplyNotification;
- /** The “Sone rescued” notification. */
- private final ListNotification<Sone> sonesRescuedNotification;
+ /** The “you have been mentioned” notification. */
+ private final ListNotification<Post> mentionNotification;
+
+ /** Notifications for sone inserts. */
+ private final Map<Sone, TemplateNotification> soneInsertNotifications = new HashMap<Sone, TemplateNotification>();
/** Sone locked notification ticker objects. */
private final Map<Sone, Object> lockedSonesTickerObjects = Collections.synchronizedMap(new HashMap<Sone, Object>());
public WebInterface(SonePlugin sonePlugin) {
this.sonePlugin = sonePlugin;
formPassword = sonePlugin.pluginRespirator().getToadletContainer().getFormPassword();
+ soneTextParser = new SoneTextParser(getCore(), getCore());
templateContextFactory = new TemplateContextFactory();
templateContextFactory.addAccessor(Object.class, new ReflectionAccessor());
templateContextFactory.addAccessor(Reply.class, new ReplyAccessor(getCore()));
templateContextFactory.addAccessor(Album.class, new AlbumAccessor());
templateContextFactory.addAccessor(Identity.class, new IdentityAccessor(getCore()));
- templateContextFactory.addAccessor(NotificationManager.class, new NotificationManagerAccessor());
templateContextFactory.addAccessor(Trust.class, new TrustAccessor());
templateContextFactory.addAccessor(HTTPRequest.class, new HttpRequestAccessor());
templateContextFactory.addFilter("date", new DateFilter());
templateContextFactory.addFilter("match", new MatchFilter());
templateContextFactory.addFilter("css", new CssClassNameFilter());
templateContextFactory.addFilter("js", new JavascriptFilter());
- templateContextFactory.addFilter("parse", new ParserFilter(getCore(), templateContextFactory));
+ templateContextFactory.addFilter("parse", new ParserFilter(getCore(), templateContextFactory, soneTextParser));
templateContextFactory.addFilter("unknown", new UnknownDateFilter(getL10n(), "View.Sone.Text.UnknownDate"));
templateContextFactory.addFilter("format", new FormatFilter());
templateContextFactory.addFilter("sort", new CollectionSortFilter());
templateContextFactory.addFilter("image-link", new ImageLinkFilter(templateContextFactory));
templateContextFactory.addFilter("replyGroup", new ReplyGroupFilter());
+ templateContextFactory.addFilter("in", new ContainsFilter());
+ templateContextFactory.addFilter("unique", new UniqueElementFilter());
templateContextFactory.addProvider(Provider.TEMPLATE_CONTEXT_PROVIDER);
templateContextFactory.addProvider(new ClassPathTemplateProvider());
+ templateContextFactory.addTemplateObject("webInterface", this);
templateContextFactory.addTemplateObject("formPassword", formPassword);
/* create notifications. */
Template newPostNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newPostNotification.html"));
newPostNotification = new ListNotification<Post>("new-post-notification", "posts", newPostNotificationTemplate, false);
+ Template localPostNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newPostNotification.html"));
+ localPostNotification = new ListNotification<Post>("local-post-notification", "posts", localPostNotificationTemplate, false);
+
Template newReplyNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newReplyNotification.html"));
- newReplyNotification = new ListNotification<Reply>("new-replies-notification", "replies", newReplyNotificationTemplate, false);
+ newReplyNotification = new ListNotification<Reply>("new-reply-notification", "replies", newReplyNotificationTemplate, false);
- Template rescuingSonesTemplate = TemplateParser.parse(createReader("/templates/notify/rescuingSonesNotification.html"));
- rescuingSonesNotification = new ListNotification<Sone>("sones-being-rescued-notification", "sones", rescuingSonesTemplate);
+ Template localReplyNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/newReplyNotification.html"));
+ localReplyNotification = new ListNotification<Reply>("local-reply-notification", "replies", localReplyNotificationTemplate, false);
- Template sonesRescuedTemplate = TemplateParser.parse(createReader("/templates/notify/sonesRescuedNotification.html"));
- sonesRescuedNotification = new ListNotification<Sone>("sones-rescued-notification", "sones", sonesRescuedTemplate);
+ Template mentionNotificationTemplate = TemplateParser.parse(createReader("/templates/notify/mentionNotification.html"));
+ mentionNotification = new ListNotification<Post>("mention-notification", "posts", mentionNotificationTemplate, false);
Template lockedSonesTemplate = TemplateParser.parse(createReader("/templates/notify/lockedSonesNotification.html"));
lockedSonesNotification = new ListNotification<Sone>("sones-locked-notification", "sones", lockedSonesTemplate);
* @return The new posts
*/
public Set<Post> getNewPosts() {
- return new HashSet<Post>(newPostNotification.getElements());
+ return new SetBuilder<Post>().addAll(newPostNotification.getElements()).addAll(localPostNotification.getElements()).get();
}
/**
* @return The new replies
*/
public Set<Reply> getNewReplies() {
- return new HashSet<Reply>(newReplyNotification.getElements());
+ return new SetBuilder<Reply>().addAll(newReplyNotification.getElements()).addAll(localReplyNotification.getElements()).get();
}
/**
Template deleteImageTemplate = TemplateParser.parse(createReader("/templates/deleteImage.html"));
Template noPermissionTemplate = TemplateParser.parse(createReader("/templates/noPermission.html"));
Template optionsTemplate = TemplateParser.parse(createReader("/templates/options.html"));
+ Template rescueTemplate = TemplateParser.parse(createReader("/templates/rescue.html"));
Template aboutTemplate = TemplateParser.parse(createReader("/templates/about.html"));
Template invalidTemplate = TemplateParser.parse(createReader("/templates/invalid.html"));
Template postTemplate = TemplateParser.parse(createReader("/templates/include/viewPost.html"));
Template openSearchTemplate = TemplateParser.parse(createReader("/templates/xml/OpenSearch.xml"));
PageToadletFactory pageToadletFactory = new PageToadletFactory(sonePlugin.pluginRespirator().getHLSimpleClient(), "/Sone/");
- pageToadlets.add(pageToadletFactory.createPageToadlet(new RedirectPage("", "index.html")));
+ pageToadlets.add(pageToadletFactory.createPageToadlet(new RedirectPage<FreenetRequest>("", "index.html")));
pageToadlets.add(pageToadletFactory.createPageToadlet(new IndexPage(indexTemplate, this), "Index"));
pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateSonePage(createSoneTemplate, this), "CreateSone"));
pageToadlets.add(pageToadletFactory.createPageToadlet(new KnownSonesPage(knownSonesTemplate, this), "KnownSones"));
pageToadlets.add(pageToadletFactory.createPageToadlet(new LoginPage(loginTemplate, this), "Login"));
pageToadlets.add(pageToadletFactory.createPageToadlet(new LogoutPage(emptyTemplate, this), "Logout"));
pageToadlets.add(pageToadletFactory.createPageToadlet(new OptionsPage(optionsTemplate, this), "Options"));
+ pageToadlets.add(pageToadletFactory.createPageToadlet(new RescuePage(rescueTemplate, this), "Rescue"));
pageToadlets.add(pageToadletFactory.createPageToadlet(new AboutPage(aboutTemplate, this, SonePlugin.VERSION), "About"));
pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("noPermission.html", noPermissionTemplate, "Page.NoPermission.Title", this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationPage(emptyTemplate, this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("invalid.html", invalidTemplate, "Page.Invalid.Title", this)));
- pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("css/", "/static/css/", "text/css")));
- pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("javascript/", "/static/javascript/", "text/javascript")));
- pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("images/", "/static/images/", "image/png")));
- pageToadlets.add(pageToadletFactory.createPageToadlet(new TemplatePage("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate)));
+ pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage<FreenetRequest>("css/", "/static/css/", "text/css")));
+ pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage<FreenetRequest>("javascript/", "/static/javascript/", "text/javascript")));
+ pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage<FreenetRequest>("images/", "/static/images/", "image/png")));
+ pageToadlets.add(pageToadletFactory.createPageToadlet(new TemplatePage<FreenetRequest>("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new GetImagePage(this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTranslationPage(this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new GetStatusAjaxPage(this)));
+ pageToadlets.add(pageToadletFactory.createPageToadlet(new GetNotificationAjaxPage(this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationAjaxPage(this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new CreatePostAjaxPage(this)));
pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateReplyAjaxPage(this)));
try {
return new InputStreamReader(getClass().getResourceAsStream(resourceName), "UTF-8");
} catch (UnsupportedEncodingException uee1) {
- System.out.println(" fail.");
return null;
}
}
- //
- // CORELISTENER METHODS
- //
-
/**
- * {@inheritDoc}
+ * Returns all {@link Core#isLocalSone(Sone) local Sone}s that are
+ * referenced by {@link SonePart}s in the given text (after parsing it using
+ * {@link SoneTextParser}).
+ *
+ * @param text
+ * The text to parse
+ * @return All mentioned local Sones
*/
- @Override
- public void rescuingSone(Sone sone) {
- rescuingSonesNotification.add(sone);
- notificationManager.addNotification(rescuingSonesNotification);
+ private Set<Sone> getMentionedSones(String text) {
+ /* we need no context to find mentioned Sones. */
+ Set<Sone> mentionedSones = new HashSet<Sone>();
+ try {
+ for (Part part : soneTextParser.parse(null, new StringReader(text))) {
+ if (part instanceof SonePart) {
+ mentionedSones.add(((SonePart) part).getSone());
+ }
+ }
+ } catch (IOException ioe1) {
+ logger.log(Level.WARNING, "Could not parse post text: " + text, ioe1);
+ }
+ return Filters.filteredSet(mentionedSones, Sone.LOCAL_SONE_FILTER);
}
/**
- * {@inheritDoc}
+ * Returns the Sone insert notification for the given Sone. If no
+ * notification for the given Sone exists, a new notification is created and
+ * cached.
+ *
+ * @param sone
+ * The Sone to get the insert notification for
+ * @return The Sone insert notification
*/
- @Override
- public void rescuedSone(Sone sone) {
- rescuingSonesNotification.remove(sone);
- sonesRescuedNotification.add(sone);
- notificationManager.addNotification(sonesRescuedNotification);
+ private TemplateNotification getSoneInsertNotification(Sone sone) {
+ synchronized (soneInsertNotifications) {
+ TemplateNotification templateNotification = soneInsertNotifications.get(sone);
+ if (templateNotification == null) {
+ templateNotification = new TemplateNotification(TemplateParser.parse(createReader("/templates/notify/soneInsertNotification.html")));
+ templateNotification.set("insertSone", sone);
+ soneInsertNotifications.put(sone, templateNotification);
+ }
+ return templateNotification;
+ }
}
+ //
+ // CORELISTENER METHODS
+ //
+
/**
* {@inheritDoc}
*/
*/
@Override
public void newPostFound(Post post) {
- newPostNotification.add(post);
+ boolean isLocal = getCore().isLocalSone(post.getSone());
+ if (isLocal) {
+ localPostNotification.add(post);
+ } else {
+ newPostNotification.add(post);
+ }
if (!hasFirstStartNotification()) {
- notificationManager.addNotification(newPostNotification);
+ notificationManager.addNotification(isLocal ? localPostNotification : newPostNotification);
+ if (!getMentionedSones(post.getText()).isEmpty() && !isLocal) {
+ mentionNotification.add(post);
+ notificationManager.addNotification(mentionNotification);
+ }
} else {
getCore().markPostKnown(post);
}
*/
@Override
public void newReplyFound(Reply reply) {
- if (reply.getPost().getSone() == null) {
- return;
+ boolean isLocal = getCore().isLocalSone(reply.getSone());
+ if (isLocal) {
+ localReplyNotification.add(reply);
+ } else {
+ newReplyNotification.add(reply);
}
- newReplyNotification.add(reply);
if (!hasFirstStartNotification()) {
- notificationManager.addNotification(newReplyNotification);
+ notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification);
+ if (!getMentionedSones(reply.getText()).isEmpty() && !isLocal && (reply.getPost().getSone() != null)) {
+ mentionNotification.add(reply.getPost());
+ notificationManager.addNotification(mentionNotification);
+ }
} else {
getCore().markReplyKnown(reply);
}
@Override
public void markPostKnown(Post post) {
newPostNotification.remove(post);
+ localPostNotification.remove(post);
+ mentionNotification.remove(post);
}
/**
@Override
public void markReplyKnown(Reply reply) {
newReplyNotification.remove(reply);
+ localReplyNotification.remove(reply);
+ mentionNotification.remove(reply.getPost());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void soneRemoved(Sone sone) {
+ newSoneNotification.remove(sone);
}
/**
@Override
public void postRemoved(Post post) {
newPostNotification.remove(post);
+ localPostNotification.remove(post);
}
/**
@Override
public void replyRemoved(Reply reply) {
newReplyNotification.remove(reply);
+ localReplyNotification.remove(reply);
}
/**
* {@inheritDoc}
*/
@Override
+ public void soneInserting(Sone sone) {
+ TemplateNotification soneInsertNotification = getSoneInsertNotification(sone);
+ soneInsertNotification.set("soneStatus", "inserting");
+ if (sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get()) {
+ notificationManager.addNotification(soneInsertNotification);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void soneInserted(Sone sone, long insertDuration) {
+ TemplateNotification soneInsertNotification = getSoneInsertNotification(sone);
+ soneInsertNotification.set("soneStatus", "inserted");
+ soneInsertNotification.set("insertDuration", insertDuration / 1000);
+ if (sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get()) {
+ notificationManager.addNotification(soneInsertNotification);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void soneInsertAborted(Sone sone, Throwable cause) {
+ TemplateNotification soneInsertNotification = getSoneInsertNotification(sone);
+ soneInsertNotification.set("soneStatus", "insert-aborted");
+ soneInsertNotification.set("insert-error", cause);
+ if (sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get()) {
+ notificationManager.addNotification(soneInsertNotification);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
public void updateFound(Version version, long releaseTime, long latestEdition) {
newVersionNotification.getTemplateContext().set("latestVersion", version);
newVersionNotification.getTemplateContext().set("latestEdition", latestEdition);
package net.pterodactylus.sone.web.ajax;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String id = request.getHttpRequest().getParam("post", null);
if ((id == null) || (id.length() == 0)) {
return createErrorJsonObject("invalid-post-id");
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.text.TextFilter;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
Sone sone = getCurrentSone(request.getToadletContext());
if (sone == null) {
return createErrorJsonObject("auth-required");
if ((text == null) || (text.trim().length() == 0)) {
return createErrorJsonObject("text-required");
}
+ text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
Post newPost = webInterface.getCore().createPost(sender, recipient, text);
return createSuccessJsonObject().put("postId", newPost.getId()).put("sone", sender.getId()).put("recipient", (newPost.getRecipient() != null) ? newPost.getRecipient().getId() : null);
}
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Reply;
import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.text.TextFilter;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String postId = request.getHttpRequest().getParam("post");
String text = request.getHttpRequest().getParam("text").trim();
String senderId = request.getHttpRequest().getParam("sender");
if ((post == null) || (post.getSone() == null)) {
return createErrorJsonObject("invalid-post-id");
}
+ text = TextFilter.filter(request.getHttpRequest().getHeader("host"), text);
Reply reply = webInterface.getCore().createReply(sender, post, text);
return createSuccessJsonObject().put("reply", reply.getId()).put("sone", sender.getId());
}
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String postId = request.getHttpRequest().getParam("post");
Post post = webInterface.getCore().getPost(postId, false);
if ((post == null) || (post.getSone() == null)) {
import net.pterodactylus.sone.data.Profile.Field;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String fieldId = request.getHttpRequest().getParam("field");
Sone currentSone = getCurrentSone(request.getToadletContext());
Profile profile = currentSone.getProfile();
}
profile.removeField(field);
currentSone.setProfile(profile);
- webInterface.getCore().saveSone(currentSone);
+ webInterface.getCore().touchConfiguration();
return createSuccessJsonObject().put("field", new JsonObject().put("id", field.getId()));
}
/*
- * Sone - DeleteReplysAjaxPage.java - Copyright © 2010 David Roden
+ * Sone - DeleteReplyAjaxPage.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
import net.pterodactylus.sone.data.Reply;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String replyId = request.getHttpRequest().getParam("reply");
Reply reply = webInterface.getCore().getReply(replyId);
if (reply == null) {
package net.pterodactylus.sone.web.ajax;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
import net.pterodactylus.util.notify.Notification;
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String notificationId = request.getHttpRequest().getParam("notification");
Notification notification = webInterface.getNotifications().getNotification(notificationId);
if (notification == null) {
/*
- * Sone - TrustAjaxPage.java - Copyright © 2011 David Roden
+ * Sone - DistrustAjaxPage.java - Copyright © 2011 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
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.freenet.wot.Trust;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
Sone currentSone = getCurrentSone(request.getToadletContext(), false);
if (currentSone == null) {
return createErrorJsonObject("auth-required");
import net.pterodactylus.sone.data.Profile.Field;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String fieldId = request.getHttpRequest().getParam("field");
Sone currentSone = getCurrentSone(request.getToadletContext());
Profile profile = currentSone.getProfile();
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String soneId = request.getHttpRequest().getParam("sone");
if (!webInterface.getCore().hasSone(soneId)) {
return createErrorJsonObject("invalid-sone-id");
return createErrorJsonObject("auth-required");
}
currentSone.addFriend(soneId);
- webInterface.getCore().saveSone(currentSone);
+ webInterface.getCore().touchConfiguration();
return createSuccessJsonObject();
}
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.template.SoneAccessor;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonArray;
import net.pterodactylus.util.json.JsonObject;
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String type = request.getHttpRequest().getParam("type", null);
String id = request.getHttpRequest().getParam(type, null);
if ((id == null) || (id.length() == 0)) {
--- /dev/null
+/*
+ * Sone - GetNotificationAjaxPage.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
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import net.pterodactylus.sone.data.Post;
+import net.pterodactylus.sone.data.Reply;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.main.SonePlugin;
+import net.pterodactylus.sone.notify.ListNotification;
+import net.pterodactylus.sone.notify.ListNotificationFilters;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.json.JsonObject;
+import net.pterodactylus.util.notify.Notification;
+import net.pterodactylus.util.notify.TemplateNotification;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * The “get notification” AJAX handler returns a number of rendered
+ * notifications.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetNotificationAjaxPage extends JsonPage {
+
+ /**
+ * Creates a new “get notification” AJAX page.
+ *
+ * @param webInterface
+ * The Sone web interface
+ */
+ public GetNotificationAjaxPage(WebInterface webInterface) {
+ super("getNotification.ajax", webInterface);
+ }
+
+ //
+ // JSONPAGE METHODS
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean needsFormPassword() {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean requiresLogin() {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ protected JsonObject createJsonObject(FreenetRequest request) {
+ String[] notificationIds = request.getHttpRequest().getParam("notifications").split(",");
+ JsonObject jsonNotifications = new JsonObject();
+ Sone currentSone = getCurrentSone(request.getToadletContext(), false);
+ for (String notificationId : notificationIds) {
+ Notification notification = webInterface.getNotifications().getNotification(notificationId);
+ if ("new-post-notification".equals(notificationId)) {
+ notification = ListNotificationFilters.filterNewPostNotification((ListNotification<Post>) notification, currentSone, false);
+ } else if ("new-reply-notification".equals(notificationId)) {
+ notification = ListNotificationFilters.filterNewReplyNotification((ListNotification<Reply>) notification, currentSone);
+ } else if ("mention-notification".equals(notificationId)) {
+ notification = ListNotificationFilters.filterNewPostNotification((ListNotification<Post>) notification, currentSone, false);
+ }
+ if (notification == null) {
+ // TODO - show error
+ continue;
+ }
+ jsonNotifications.put(notificationId, createJsonNotification(request, notification));
+ }
+ return createSuccessJsonObject().put("notifications", jsonNotifications);
+ }
+
+ //
+ // PRIVATE METHODS
+ //
+
+ /**
+ * Creates a JSON object from the given notification.
+ *
+ * @param request
+ * The request to load the session from
+ * @param notification
+ * The notification to create a JSON object
+ * @return The JSON object
+ */
+ private JsonObject createJsonNotification(FreenetRequest request, Notification notification) {
+ JsonObject jsonNotification = new JsonObject();
+ jsonNotification.put("id", notification.getId());
+ StringWriter notificationWriter = new StringWriter();
+ try {
+ if (notification instanceof TemplateNotification) {
+ TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext().mergeContext(((TemplateNotification) notification).getTemplateContext());
+ templateContext.set("core", webInterface.getCore());
+ templateContext.set("currentSone", webInterface.getCurrentSone(request.getToadletContext(), false));
+ templateContext.set("localSones", webInterface.getCore().getLocalSones());
+ templateContext.set("request", request);
+ templateContext.set("currentVersion", SonePlugin.VERSION);
+ templateContext.set("hasLatestVersion", webInterface.getCore().getUpdateChecker().hasLatestVersion());
+ templateContext.set("latestEdition", webInterface.getCore().getUpdateChecker().getLatestEdition());
+ templateContext.set("latestVersion", webInterface.getCore().getUpdateChecker().getLatestVersion());
+ templateContext.set("latestVersionTime", webInterface.getCore().getUpdateChecker().getLatestVersionDate());
+ templateContext.set("notification", notification);
+ ((TemplateNotification) notification).render(templateContext, notificationWriter);
+ } else {
+ notification.render(notificationWriter);
+ }
+ } catch (IOException ioe1) {
+ /* StringWriter never throws, ignore. */
+ }
+ jsonNotification.put("text", notificationWriter.toString());
+ jsonNotification.put("createdTime", notification.getCreatedTime());
+ jsonNotification.put("lastUpdatedTime", notification.getLastUpdatedTime());
+ jsonNotification.put("dismissable", notification.isDismissable());
+ return jsonNotification;
+ }
+
+}
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.io.Closer;
import net.pterodactylus.util.json.JsonObject;
import net.pterodactylus.util.template.Template;
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String postId = request.getHttpRequest().getParam("post");
Post post = webInterface.getCore().getPost(postId, false);
if (post == null) {
return createErrorJsonObject("invalid-post-id");
}
- return createSuccessJsonObject().put("post", createJsonPost(post, getCurrentSone(request.getToadletContext())));
+ return createSuccessJsonObject().put("post", createJsonPost(request, post, getCurrentSone(request.getToadletContext())));
}
/**
* Creates a JSON object from the given post. The JSON object will only
* contain the ID of the post, its time, and its rendered HTML code.
*
+ * @param request
+ * The request being processed
* @param post
* The post to create a JSON object from
* @param currentSone
* The currently logged in Sone (to store in the template)
* @return The JSON representation of the post
*/
- private JsonObject createJsonPost(Post post, Sone currentSone) {
+ private JsonObject createJsonPost(FreenetRequest request, Post post, Sone currentSone) {
JsonObject jsonPost = new JsonObject();
jsonPost.put("id", post.getId());
jsonPost.put("sone", post.getSone().getId());
jsonPost.put("time", post.getTime());
StringWriter stringWriter = new StringWriter();
TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext();
+ templateContext.set("core", webInterface.getCore());
+ templateContext.set("request", request);
templateContext.set("post", post);
templateContext.set("currentSone", currentSone);
templateContext.set("localSones", webInterface.getCore().getLocalSones());
import net.pterodactylus.sone.data.Reply;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.io.Closer;
import net.pterodactylus.util.json.JsonObject;
import net.pterodactylus.util.template.Template;
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String replyId = request.getHttpRequest().getParam("reply");
Reply reply = webInterface.getCore().getReply(replyId);
if ((reply == null) || (reply.getSone() == null)) {
return createErrorJsonObject("invalid-reply-id");
}
- return createSuccessJsonObject().put("reply", createJsonReply(reply, getCurrentSone(request.getToadletContext())));
+ return createSuccessJsonObject().put("reply", createJsonReply(request, reply, getCurrentSone(request.getToadletContext())));
}
/**
/**
* Creates a JSON representation of the given reply.
*
+ * @param request
+ * The request being processed
* @param reply
* The reply to convert
* @param currentSone
* The currently logged in Sone (to store in the template)
* @return The JSON representation of the reply
*/
- private JsonObject createJsonReply(Reply reply, Sone currentSone) {
+ private JsonObject createJsonReply(FreenetRequest request, Reply reply, Sone currentSone) {
JsonObject jsonReply = new JsonObject();
jsonReply.put("id", reply.getId());
jsonReply.put("postId", reply.getPost().getId());
jsonReply.put("time", reply.getTime());
StringWriter stringWriter = new StringWriter();
TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext();
+ templateContext.set("core", webInterface.getCore());
+ templateContext.set("request", request);
templateContext.set("reply", reply);
templateContext.set("currentSone", currentSone);
try {
package net.pterodactylus.sone.web.ajax;
-import java.io.IOException;
-import java.io.StringWriter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import net.pterodactylus.sone.notify.ListNotificationFilters;
import net.pterodactylus.sone.template.SoneAccessor;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.filter.Filter;
import net.pterodactylus.util.filter.Filters;
import net.pterodactylus.util.json.JsonArray;
import net.pterodactylus.util.json.JsonObject;
import net.pterodactylus.util.notify.Notification;
-import net.pterodactylus.util.notify.TemplateNotification;
-import net.pterodactylus.util.template.TemplateContext;
/**
* The “get status” AJAX handler returns all information that is necessary to
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
final Sone currentSone = getCurrentSone(request.getToadletContext(), false);
/* load Sones. */
- boolean loadAllSones = Boolean.parseBoolean(request.getHttpRequest().getParam("loadAllSones", "true"));
+ boolean loadAllSones = Boolean.parseBoolean(request.getHttpRequest().getParam("loadAllSones", "false"));
Set<Sone> sones = new HashSet<Sone>(Collections.singleton(getCurrentSone(request.getToadletContext(), false)));
if (loadAllSones) {
sones.addAll(webInterface.getCore().getSones());
+ } else {
+ String loadSoneIds = request.getHttpRequest().getParam("soneIds");
+ if (loadSoneIds.length() > 0) {
+ String[] soneIds = loadSoneIds.split(",");
+ for (String soneId : soneIds) {
+ /* just add it, we skip null further down. */
+ sones.add(webInterface.getCore().getSone(soneId, false));
+ }
+ }
}
JsonArray jsonSones = new JsonArray();
for (Sone sone : sones) {
jsonSones.add(jsonSone);
}
/* load notifications. */
- List<Notification> notifications = ListNotificationFilters.filterNotifications(new ArrayList<Notification>(webInterface.getNotifications().getNotifications()), currentSone);
+ List<Notification> notifications = ListNotificationFilters.filterNotifications(webInterface.getNotifications().getNotifications(), currentSone);
Collections.sort(notifications, Notification.LAST_UPDATED_TIME_SORTER);
- JsonArray jsonNotifications = new JsonArray();
+ JsonArray jsonNotificationInformations = new JsonArray();
for (Notification notification : notifications) {
- jsonNotifications.add(createJsonNotification(notification));
+ jsonNotificationInformations.add(createJsonNotificationInformation(notification));
}
/* load new posts. */
Set<Post> newPosts = webInterface.getNewPosts();
@Override
public boolean filterObject(Post post) {
- return currentSone.hasFriend(post.getSone().getId()) || currentSone.equals(post.getSone()) || currentSone.equals(post.getRecipient());
+ return ListNotificationFilters.isPostVisible(currentSone, post);
}
});
@Override
public boolean filterObject(Reply reply) {
- return currentSone.hasFriend(reply.getPost().getSone().getId()) || currentSone.equals(reply.getPost().getSone()) || currentSone.equals(reply.getPost().getRecipient());
+ return ListNotificationFilters.isReplyVisible(currentSone, reply);
}
});
}
+ /* remove replies to unknown posts. */
+ newReplies = Filters.filteredSet(newReplies, new Filter<Reply>() {
+
+ @Override
+ public boolean filterObject(Reply reply) {
+ return (reply.getPost() != null) && (reply.getPost().getSone() != null);
+ }
+ });
JsonArray jsonReplies = new JsonArray();
for (Reply reply : newReplies) {
JsonObject jsonReply = new JsonObject();
jsonReply.put("postSone", reply.getPost().getSone().getId());
jsonReplies.add(jsonReply);
}
- return createSuccessJsonObject().put("sones", jsonSones).put("notifications", jsonNotifications).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
+ return createSuccessJsonObject().put("loggedIn", currentSone != null).put("sones", jsonSones).put("notifications", jsonNotificationInformations).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
}
/**
synchronized (dateFormat) {
jsonSone.put("lastUpdated", dateFormat.format(new Date(sone.getTime())));
}
- jsonSone.put("age", (System.currentTimeMillis() - sone.getTime()) / 1000);
+ jsonSone.put("lastUpdatedText", GetTimesAjaxPage.getTime(webInterface, sone.getTime()).getText());
return jsonSone;
}
/**
- * Creates a JSON object from the given notification.
+ * Creates a JSON object that only contains the ID and the last-updated time
+ * of the given notification.
*
+ * @see Notification#getId()
+ * @see Notification#getLastUpdatedTime()
* @param notification
- * The notification to create a JSON object
- * @return The JSON object
+ * The notification
+ * @return A JSON object containing the notification ID and last-updated
+ * time
*/
- private JsonObject createJsonNotification(Notification notification) {
+ private JsonObject createJsonNotificationInformation(Notification notification) {
JsonObject jsonNotification = new JsonObject();
jsonNotification.put("id", notification.getId());
- StringWriter notificationWriter = new StringWriter();
- try {
- if (notification instanceof TemplateNotification) {
- TemplateContext templateContext = webInterface.getTemplateContextFactory().createTemplateContext().mergeContext(((TemplateNotification) notification).getTemplateContext());
- templateContext.set("notification", notification);
- ((TemplateNotification) notification).render(templateContext, notificationWriter);
- } else {
- notification.render(notificationWriter);
- }
- } catch (IOException ioe1) {
- /* StringWriter never throws, ignore. */
- }
- jsonNotification.put("text", notificationWriter.toString());
- jsonNotification.put("createdTime", notification.getCreatedTime());
jsonNotification.put("lastUpdatedTime", notification.getLastUpdatedTime());
- jsonNotification.put("dismissable", notification.isDismissable());
return jsonNotification;
}
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Reply;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
import net.pterodactylus.util.number.Digits;
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
- long now = System.currentTimeMillis();
+ protected JsonObject createJsonObject(FreenetRequest request) {
String allIds = request.getHttpRequest().getParam("posts");
JsonObject postTimes = new JsonObject();
if (allIds.length() > 0) {
if (post == null) {
continue;
}
- long age = now - post.getTime();
JsonObject postTime = new JsonObject();
- Time time = getTime(age);
+ Time time = getTime(post.getTime());
postTime.put("timeText", time.getText());
postTime.put("refreshTime", time.getRefresh() / Time.SECOND);
postTime.put("tooltip", dateFormat.format(new Date(post.getTime())));
if (reply == null) {
continue;
}
- long age = now - reply.getTime();
JsonObject replyTime = new JsonObject();
- Time time = getTime(age);
+ Time time = getTime(reply.getTime());
replyTime.put("timeText", time.getText());
replyTime.put("refreshTime", time.getRefresh() / Time.SECOND);
replyTime.put("tooltip", dateFormat.format(new Date(reply.getTime())));
//
/**
- * Returns the formatted relative time for a given age.
+ * Returns the formatted relative time for a given time.
*
- * @param age
- * The age to format (in milliseconds)
+ * @param time
+ * The time to format the difference from (in milliseconds)
* @return The formatted age
*/
- private Time getTime(long age) {
+ private Time getTime(long time) {
+ return getTime(webInterface, time);
+ }
+
+ //
+ // STATIC METHODS
+ //
+
+ /**
+ * Returns the formatted relative time for a given time.
+ *
+ * @param webInterface
+ * The Sone web interface (for l10n access)
+ * @param time
+ * The time to format the difference from (in milliseconds)
+ * @return The formatted age
+ */
+ public static Time getTime(WebInterface webInterface, long time) {
+ if (time == 0) {
+ return new Time(webInterface.getL10n().getString("View.Sone.Text.UnknownDate"), 12 * Time.HOUR);
+ }
+ long age = System.currentTimeMillis() - time;
String text;
long refresh;
if (age < 0) {
text = webInterface.getL10n().getString("View.Time.AMinuteAgo");
refresh = Time.MINUTE;
} else if (age < 30 * Time.MINUTE) {
- text = webInterface.getL10n().getString("View.Time.XMinutesAgo", "min", String.valueOf((int) Digits.round(age / Time.MINUTE, 1)));
+ text = webInterface.getL10n().getString("View.Time.XMinutesAgo", "min", String.valueOf((int) (Digits.round(age, Time.MINUTE) / Time.MINUTE)));
refresh = 1 * Time.MINUTE;
} else if (age < 45 * Time.MINUTE) {
text = webInterface.getL10n().getString("View.Time.HalfAnHourAgo");
text = webInterface.getL10n().getString("View.Time.AnHourAgo");
refresh = Time.HOUR;
} else if (age < 21 * Time.HOUR) {
- text = webInterface.getL10n().getString("View.Time.XHoursAgo", "hour", String.valueOf((int) Digits.round(age / Time.HOUR, 1)));
+ text = webInterface.getL10n().getString("View.Time.XHoursAgo", "hour", String.valueOf((int) (Digits.round(age, Time.HOUR) / Time.HOUR)));
refresh = Time.HOUR;
} else if (age < 42 * Time.HOUR) {
text = webInterface.getL10n().getString("View.Time.ADayAgo");
refresh = Time.DAY;
} else if (age < 6 * Time.DAY) {
- text = webInterface.getL10n().getString("View.Time.XDaysAgo", "day", String.valueOf((int) Digits.round(age / Time.DAY, 1)));
+ text = webInterface.getL10n().getString("View.Time.XDaysAgo", "day", String.valueOf((int) (Digits.round(age, Time.DAY) / Time.DAY)));
refresh = Time.DAY;
} else if (age < 11 * Time.DAY) {
text = webInterface.getL10n().getString("View.Time.AWeekAgo");
refresh = Time.DAY;
} else if (age < 4 * Time.WEEK) {
- text = webInterface.getL10n().getString("View.Time.XWeeksAgo", "week", String.valueOf((int) Digits.round(age / Time.WEEK, 1)));
+ text = webInterface.getL10n().getString("View.Time.XWeeksAgo", "week", String.valueOf((int) (Digits.round(age, Time.WEEK) / Time.WEEK)));
refresh = Time.DAY;
} else if (age < 6 * Time.WEEK) {
text = webInterface.getL10n().getString("View.Time.AMonthAgo");
refresh = Time.DAY;
} else if (age < 11 * Time.MONTH) {
- text = webInterface.getL10n().getString("View.Time.XMonthsAgo", "month", String.valueOf((int) Digits.round(age / Time.MONTH, 1)));
+ text = webInterface.getL10n().getString("View.Time.XMonthsAgo", "month", String.valueOf((int) (Digits.round(age, Time.MONTH) / Time.MONTH)));
refresh = Time.DAY;
} else if (age < 18 * Time.MONTH) {
text = webInterface.getL10n().getString("View.Time.AYearAgo");
refresh = Time.WEEK;
} else {
- text = webInterface.getL10n().getString("View.Time.XYearsAgo", "year", String.valueOf((int) Digits.round(age / Time.YEAR, 1)));
+ text = webInterface.getL10n().getString("View.Time.XYearsAgo", "year", String.valueOf((int) (Digits.round(age, Time.YEAR) / Time.YEAR)));
refresh = Time.WEEK;
}
return new Time(text, refresh);
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
- private static class Time {
+ public static class Time {
/** Number of milliseconds in a second. */
private static final long SECOND = 1000;
return refresh;
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ return text;
+ }
+
}
}
package net.pterodactylus.sone.web.ajax;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String key = request.getHttpRequest().getParam("key");
String translation = webInterface.getL10n().getString(key);
return createSuccessJsonObject().put("value", translation);
package net.pterodactylus.sone.web.ajax;
+import java.io.IOException;
+
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.web.WebInterface;
-import net.pterodactylus.sone.web.page.Page;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
import net.pterodactylus.util.json.JsonUtils;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Response;
import freenet.clients.http.SessionManager.Session;
import freenet.clients.http.ToadletContext;
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
-public abstract class JsonPage implements Page {
+public abstract class JsonPage implements Page<FreenetRequest> {
/** The path of the page. */
private final String path;
* The request to handle
* @return The created JSON object
*/
- protected abstract JsonObject createJsonObject(Request request);
+ protected abstract JsonObject createJsonObject(FreenetRequest request);
/**
* Returns whether this command needs the form password for authentication
* {@inheritDoc}
*/
@Override
- public Response handleRequest(Request request) {
+ public boolean isPrefixPage() {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response handleRequest(FreenetRequest request, Response response) throws IOException {
+ if (webInterface.getCore().getPreferences().isRequireFullAccess() && !request.getToadletContext().isAllowedFullAccess()) {
+ return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
+ }
if (needsFormPassword()) {
String formPassword = request.getHttpRequest().getParam("formPassword");
if (!webInterface.getFormPassword().equals(formPassword)) {
- return new Response(401, "Not authorized", "application/json", JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
+ return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
}
}
if (requiresLogin()) {
if (getCurrentSone(request.getToadletContext(), false) == null) {
- return new Response(401, "Not authorized", "application/json", JsonUtils.format(createErrorJsonObject("auth-required")));
+ return response.setStatusCode(403).setStatusText("Forbidden").setContentType("application/json").write(JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
}
}
JsonObject jsonObject = createJsonObject(request);
- return new Response(200, "OK", "application/json", JsonUtils.format(jsonObject));
+ return response.setStatusCode(200).setStatusText("OK").setContentType("application/json").write(JsonUtils.format(jsonObject));
}
}
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String type = request.getHttpRequest().getParam("type", null);
String id = request.getHttpRequest().getParam(type, null);
if ((id == null) || (id.length() == 0)) {
}
if ("post".equals(type)) {
currentSone.addLikedPostId(id);
- webInterface.getCore().saveSone(currentSone);
+ webInterface.getCore().touchConfiguration();
} else if ("reply".equals(type)) {
currentSone.addLikedReplyId(id);
- webInterface.getCore().saveSone(currentSone);
+ webInterface.getCore().touchConfiguration();
} else {
return createErrorJsonObject("invalid-type");
}
import net.pterodactylus.sone.core.Core;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String soneId = request.getHttpRequest().getParam("sone");
Sone sone = webInterface.getCore().getLocalSone(soneId, false);
if (sone == null) {
import net.pterodactylus.sone.data.Reply;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String type = request.getHttpRequest().getParam("type");
if (!type.equals("sone") && !type.equals("post") && !type.equals("reply")) {
return createErrorJsonObject("invalid-type");
import net.pterodactylus.sone.data.Profile.Field;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
Sone currentSone = getCurrentSone(request.getToadletContext());
Profile profile = currentSone.getProfile();
String fieldId = request.getHttpRequest().getParam("field");
return createErrorJsonObject("not-possible");
}
currentSone.setProfile(profile);
- webInterface.getCore().saveSone(currentSone);
+ webInterface.getCore().touchConfiguration();
return createSuccessJsonObject();
}
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.freenet.wot.Trust;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
Sone currentSone = getCurrentSone(request.getToadletContext(), false);
if (currentSone == null) {
return createErrorJsonObject("auth-required");
/*
- * Sone - BookmarkAjaxPage.java - Copyright © 2011 David Roden
+ * Sone - UnbookmarkAjaxPage.java - Copyright © 2011 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
package net.pterodactylus.sone.web.ajax;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String id = request.getHttpRequest().getParam("post", null);
if ((id == null) || (id.length() == 0)) {
return createErrorJsonObject("invalid-post-id");
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String soneId = request.getHttpRequest().getParam("sone");
if (!webInterface.getCore().hasSone(soneId)) {
return createErrorJsonObject("invalid-sone-id");
return createErrorJsonObject("auth-required");
}
currentSone.removeFriend(soneId);
- webInterface.getCore().saveSone(currentSone);
+ webInterface.getCore().touchConfiguration();
return createSuccessJsonObject();
}
import net.pterodactylus.sone.data.Post;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String type = request.getHttpRequest().getParam("type", null);
String id = request.getHttpRequest().getParam(type, null);
if ((id == null) || (id.length() == 0)) {
}
if ("post".equals(type)) {
currentSone.removeLikedPostId(id);
- webInterface.getCore().saveSone(currentSone);
+ webInterface.getCore().touchConfiguration();
} else if ("reply".equals(type)) {
currentSone.removeLikedReplyId(id);
- webInterface.getCore().saveSone(currentSone);
+ webInterface.getCore().touchConfiguration();
} else {
return createErrorJsonObject("invalid-type");
}
/*
- * Sone - LockSoneAjaxPage.java - Copyright © 2010 David Roden
+ * Sone - UnlockSoneAjaxPage.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
import net.pterodactylus.sone.core.Core;
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
String soneId = request.getHttpRequest().getParam("sone");
Sone sone = webInterface.getCore().getLocalSone(soneId, false);
if (sone == null) {
/*
- * Sone - TrustAjaxPage.java - Copyright © 2011 David Roden
+ * Sone - UntrustAjaxPage.java - Copyright © 2011 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
import net.pterodactylus.sone.data.Sone;
import net.pterodactylus.sone.freenet.wot.Trust;
import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
import net.pterodactylus.util.json.JsonObject;
/**
* {@inheritDoc}
*/
@Override
- protected JsonObject createJsonObject(Request request) {
+ protected JsonObject createJsonObject(FreenetRequest request) {
Sone currentSone = getCurrentSone(request.getToadletContext(), false);
if (currentSone == null) {
return createErrorJsonObject("auth-required");
--- /dev/null
+/*
+ * Sone - FreenetRequest.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.page;
+
+import java.net.URI;
+
+import net.pterodactylus.util.web.Method;
+import net.pterodactylus.util.web.Request;
+import freenet.clients.http.ToadletContext;
+import freenet.support.api.HTTPRequest;
+
+/**
+ * Encapsulates all Freenet-specific properties of a request.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class FreenetRequest extends Request {
+
+ /** The underlying HTTP request from Freenet. */
+ private final HTTPRequest httpRequest;
+
+ /** The toadlet context. */
+ private final ToadletContext toadletContext;
+
+ /**
+ * Creates a new freenet request.
+ *
+ * @param uri
+ * The URI that is being accessed
+ * @param method
+ * The method used to access this page
+ * @param httpRequest
+ * The underlying HTTP request from Freenet
+ * @param toadletContext
+ * The toadlet context
+ */
+ public FreenetRequest(URI uri, Method method, HTTPRequest httpRequest, ToadletContext toadletContext) {
+ super(uri, method);
+ this.httpRequest = httpRequest;
+ this.toadletContext = toadletContext;
+ }
+
+ //
+ // ACCESSORS
+ //
+
+ /**
+ * Returns the underlying HTTP request from Freenet.
+ *
+ * @return The underlying HTTP request from Freenet
+ */
+ public HTTPRequest getHttpRequest() {
+ return httpRequest;
+ }
+
+ /**
+ * Returns the toadlet context.
+ *
+ * @return The toadlet context
+ */
+ public ToadletContext getToadletContext() {
+ return toadletContext;
+ }
+
+}
/*
- * shortener - TemplatePage.java - Copyright © 2010 David Roden
+ * Sone - FreenetTemplatePage.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
package net.pterodactylus.sone.web.page;
+import java.io.IOException;
import java.io.StringWriter;
import java.util.Collection;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.Logger;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
import net.pterodactylus.util.logging.Logging;
import net.pterodactylus.util.template.Template;
import net.pterodactylus.util.template.TemplateContext;
import net.pterodactylus.util.template.TemplateContextFactory;
+import net.pterodactylus.util.web.Method;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.RedirectResponse;
+import net.pterodactylus.util.web.Response;
import freenet.clients.http.LinkEnabledCallback;
import freenet.clients.http.PageMaker;
import freenet.clients.http.PageNode;
*
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
-public class FreenetTemplatePage implements Page, LinkEnabledCallback {
+public class FreenetTemplatePage implements Page<FreenetRequest>, LinkEnabledCallback {
/** The logger. */
private static final Logger logger = Logging.getLogger(FreenetTemplatePage.class);
* The request to serve
* @return The title of the page
*/
- protected String getPageTitle(Request request) {
+ protected String getPageTitle(FreenetRequest request) {
return null;
}
* {@inheritDoc}
*/
@Override
- public Response handleRequest(Request request) {
+ public boolean isPrefixPage() {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Response handleRequest(FreenetRequest request, Response response) throws IOException {
String redirectTarget = getRedirectTarget(request);
if (redirectTarget != null) {
return new RedirectResponse(redirectTarget);
}
+ if (isFullAccessOnly() && !request.getToadletContext().isAllowedFullAccess()) {
+ return response.setStatusCode(401).setStatusText("Not authorized").setContentType("text/html");
+ }
ToadletContext toadletContext = request.getToadletContext();
if (request.getMethod() == Method.POST) {
/* require form password. */
postProcess(request, templateContext);
- return new Response(200, "OK", "text/html", pageNode.outer.generate());
+ return response.setStatusCode(200).setStatusText("OK").setContentType("text/html").write(pageNode.outer.generate());
}
/**
* @throws RedirectException
* if the processing page wants to redirect after processing
*/
- protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException {
+ protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
/* do nothing. */
}
* @param templateContext
* The template context that supplied the rendered data
*/
- protected void postProcess(Request request, TemplateContext templateContext) {
+ protected void postProcess(FreenetRequest request, TemplateContext templateContext) {
/* do nothing. */
}
* The request that is processed
* @return The URL to redirect to, or {@code null} to not redirect
*/
- protected String getRedirectTarget(Page.Request request) {
+ protected String getRedirectTarget(FreenetRequest request) {
return null;
}
* The request for which to return the link nodes
* @return All link nodes that should be added to the HTML head
*/
- protected List<Map<String, String>> getAdditionalLinkNodes(Request request) {
+ protected List<Map<String, String>> getAdditionalLinkNodes(FreenetRequest request) {
return Collections.emptyList();
}
+ /**
+ * Returns whether this page should only be allowed for requests from hosts
+ * with full access.
+ *
+ * @return {@code true} if this page should only be allowed for hosts with
+ * full access, {@code false} to allow this page for any host
+ */
+ protected boolean isFullAccessOnly() {
+ return false;
+ }
+
//
// INTERFACE LinkEnabledCallback
//
*/
@Override
public boolean isEnabled(ToadletContext toadletContext) {
- return true;
+ return !isFullAccessOnly();
}
/**
+++ /dev/null
-/*
- * shortener - Page.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
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web.page;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.util.HashMap;
-import java.util.Map;
-
-import freenet.clients.http.ToadletContext;
-import freenet.support.api.HTTPRequest;
-
-/**
- * A page is responsible for handling HTTP requests and creating appropriate
- * responses.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public interface Page {
-
- /**
- * Returns the path of this toadlet.
- *
- * @return The path of this toadlet
- */
- public String getPath();
-
- /**
- * Handles a request.
- *
- * @param request
- * The request to handle
- * @return The response
- */
- public Response handleRequest(Request request);
-
- /**
- * Container for request data.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
- public class Request {
-
- /**
- * Enumeration for all possible HTTP request methods.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’
- * Roden</a>
- */
- public enum Method {
-
- /** GET. */
- GET,
-
- /** POST. */
- POST,
-
- /** PUT. */
- PUT,
-
- /** DELETE. */
- DELETE,
-
- /** HEAD. */
- HEAD,
-
- /** OPTIONS. */
- OPTIONS,
-
- /** TRACE. */
- TRACE,
-
- }
-
- /** The URI that was accessed. */
- private final URI uri;
-
- /** The HTTP method that was used. */
- private final Method method;
-
- /** The HTTP request. */
- private final HTTPRequest httpRequest;
-
- /** The toadlet context. */
- private final ToadletContext toadletContext;
-
- /**
- * Creates a new request that holds the given data.
- *
- * @param uri
- * The URI of the request
- * @param method
- * The HTTP method of the request
- * @param httpRequest
- * The HTTP request
- * @param toadletContext
- * The toadlet context of the request
- */
- public Request(URI uri, Method method, HTTPRequest httpRequest, ToadletContext toadletContext) {
- this.uri = uri;
- this.method = method;
- this.httpRequest = httpRequest;
- this.toadletContext = toadletContext;
- }
-
- /**
- * Returns the URI that was accessed.
- *
- * @return The accessed URI
- */
- public URI getUri() {
- return uri;
- }
-
- /**
- * Returns the HTTP method that was used to access the page.
- *
- * @return The HTTP method
- */
- public Method getMethod() {
- return method;
- }
-
- /**
- * Returns the HTTP request.
- *
- * @return The HTTP request
- */
- public HTTPRequest getHttpRequest() {
- return httpRequest;
- }
-
- /**
- * Returns the toadlet context.
- *
- * @return The toadlet context
- */
- public ToadletContext getToadletContext() {
- return toadletContext;
- }
-
- }
-
- /**
- * Container for the HTTP response of a {@link Page}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
- public class Response {
-
- /** The HTTP status code of the response. */
- private final int statusCode;
-
- /** The HTTP status text of the response. */
- private final String statusText;
-
- /** The content type of the response. */
- private final String contentType;
-
- /** The headers of the response. */
- private final Map<String, String> headers;
-
- /** The content of the response body. */
- private final InputStream content;
-
- /**
- * Creates a new response.
- *
- * @param statusCode
- * The HTTP status code of the response
- * @param statusText
- * The HTTP status text of the response
- * @param contentType
- * The content type of the response
- * @param text
- * The text in the response body
- */
- public Response(int statusCode, String statusText, String contentType, String text) {
- this(statusCode, statusText, contentType, getBytes(text));
- }
-
- /**
- * Creates a new response.
- *
- * @param statusCode
- * The HTTP status code of the response
- * @param statusText
- * The HTTP status text of the response
- * @param contentType
- * The content type of the response
- * @param content
- * The content of the reponse body
- */
- public Response(int statusCode, String statusText, String contentType, byte[] content) {
- this(statusCode, statusText, contentType, new HashMap<String, String>(), content);
- }
-
- /**
- * Creates a new response.
- *
- * @param statusCode
- * The HTTP status code of the response
- * @param statusText
- * The HTTP status text of the response
- * @param contentType
- * The content type of the response
- * @param headers
- * The headers of the response
- */
- public Response(int statusCode, String statusText, String contentType, Map<String, String> headers) {
- this(statusCode, statusText, contentType, headers, (InputStream) null);
- }
-
- /**
- * Creates a new response.
- *
- * @param statusCode
- * The HTTP status code of the response
- * @param statusText
- * The HTTP status text of the response
- * @param contentType
- * The content type of the response
- * @param headers
- * The headers of the response
- * @param content
- * The content of the reponse body
- */
- public Response(int statusCode, String statusText, String contentType, Map<String, String> headers, byte[] content) {
- this(statusCode, statusText, contentType, headers, new ByteArrayInputStream(content));
- }
-
- /**
- * Creates a new response.
- *
- * @param statusCode
- * The HTTP status code of the response
- * @param statusText
- * The HTTP status text of the response
- * @param contentType
- * The content type of the response
- * @param headers
- * The headers of the response
- * @param content
- * The content of the reponse body
- */
- public Response(int statusCode, String statusText, String contentType, Map<String, String> headers, InputStream content) {
- this.statusCode = statusCode;
- this.statusText = statusText;
- this.contentType = contentType;
- this.headers = headers;
- this.content = content;
- }
-
- /**
- * Returns the HTTP status code of the response.
- *
- * @return The HTTP status code
- */
- public int getStatusCode() {
- return statusCode;
- }
-
- /**
- * Returns the HTTP status text.
- *
- * @return The HTTP status text
- */
- public String getStatusText() {
- return statusText;
- }
-
- /**
- * Returns the content type of the response.
- *
- * @return The content type of the reponse
- */
- public String getContentType() {
- return contentType;
- }
-
- /**
- * Returns HTTP headers of the response. May be {@code null} if no
- * headers are returned.
- *
- * @return The response headers, or {@code null} if there are no
- * response headers
- */
- public Map<String, String> getHeaders() {
- return headers;
- }
-
- /**
- * Sets the HTTP header with the given name to the given value. Multiple
- * headers with the same name are not implemented so that latest call to
- * {@link #setHeader(String, String)} determines what is sent to the
- * browser.
- *
- * @param name
- * The name of the header
- * @param value
- * The value of the header
- * @return This response
- */
- public Response setHeader(String name, String value) {
- headers.put(name, value);
- return this;
- }
-
- /**
- * Returns the content of the response body. May be {@code null} if the
- * response does not have a body.
- *
- * @return The content of the response body
- */
- public InputStream getContent() {
- return content;
- }
-
- //
- // PRIVATE METHODS
- //
-
- /**
- * Returns the UTF-8 representation of the given text.
- *
- * @param text
- * The text to encode
- * @return The encoded text
- */
- private static byte[] getBytes(String text) {
- try {
- return text.getBytes("UTF-8");
- } catch (UnsupportedEncodingException uee1) {
- /* every JVM needs to support UTF-8. */
- }
- return null;
- }
-
- /**
- * Creates a header map containing a single header.
- *
- * @param name
- * The name of the header
- * @param value
- * The value of the header
- * @return The map containing the single header
- */
- protected static Map<String, String> createHeader(String name, String value) {
- Map<String, String> headers = new HashMap<String, String>();
- headers.put(name, value);
- return headers;
- }
-
- }
-
- /**
- * {@link Response} implementation that performs an HTTP redirect.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
- public class RedirectResponse extends Response {
-
- /**
- * Creates a new redirect response to the new location.
- *
- * @param newLocation
- * The new location
- */
- public RedirectResponse(String newLocation) {
- this(newLocation, true);
- }
-
- /**
- * Creates a new redirect response to the new location.
- *
- * @param newLocation
- * The new location
- * @param permanent
- * Whether the redirect should be marked as permanent
- */
- public RedirectResponse(String newLocation, boolean permanent) {
- super(permanent ? 302 : 307, "Redirected", null, createHeader("Location", newLocation));
- }
-
- }
-
-}
/*
- * shortener - PageToadlet.java - Copyright © 2010 David Roden
+ * Sone - PageToadlet.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
import java.io.IOException;
import java.net.URI;
-import java.util.Map.Entry;
-import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.web.Header;
+import net.pterodactylus.util.web.Method;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Response;
import freenet.client.HighLevelSimpleClient;
import freenet.clients.http.LinkEnabledCallback;
import freenet.clients.http.Toadlet;
import freenet.support.MultiValueTable;
import freenet.support.api.Bucket;
import freenet.support.api.HTTPRequest;
-import freenet.support.io.BucketTools;
import freenet.support.io.Closer;
/**
private final String menuName;
/** The page that handles processing. */
- private final Page page;
+ private final Page<FreenetRequest> page;
/** The path prefix for the page. */
private final String pathPrefix;
* Prefix that is prepended to all {@link Page#getPath()} return
* values
*/
- protected PageToadlet(HighLevelSimpleClient highLevelSimpleClient, String menuName, Page page, String pathPrefix) {
+ protected PageToadlet(HighLevelSimpleClient highLevelSimpleClient, String menuName, Page<FreenetRequest> page, String pathPrefix) {
super(highLevelSimpleClient);
this.menuName = menuName;
this.page = page;
* if the toadlet context is closed
*/
public void handleMethodGET(URI uri, HTTPRequest httpRequest, ToadletContext toadletContext) throws IOException, ToadletContextClosedException {
- handleRequest(new Page.Request(uri, Method.GET, httpRequest, toadletContext));
+ handleRequest(new FreenetRequest(uri, Method.GET, httpRequest, toadletContext));
}
/**
* if the toadlet context is closed
*/
public void handleMethodPOST(URI uri, HTTPRequest httpRequest, ToadletContext toadletContext) throws IOException, ToadletContextClosedException {
- handleRequest(new Page.Request(uri, Method.POST, httpRequest, toadletContext));
+ handleRequest(new FreenetRequest(uri, Method.POST, httpRequest, toadletContext));
}
/**
* @throws ToadletContextClosedException
* if the toadlet context is closed
*/
- private void handleRequest(Page.Request pageRequest) throws IOException, ToadletContextClosedException {
- Bucket data = null;
+ private void handleRequest(FreenetRequest pageRequest) throws IOException, ToadletContextClosedException {
+ Bucket pageBucket = null;
try {
- Page.Response pageResponse = page.handleRequest(pageRequest);
+ pageBucket = pageRequest.getToadletContext().getBucketFactory().makeBucket(-1);
+ Response pageResponse = new Response(pageBucket.getOutputStream());
+ pageResponse = page.handleRequest(pageRequest, pageResponse);
MultiValueTable<String, String> headers = new MultiValueTable<String, String>();
if (pageResponse.getHeaders() != null) {
- for (Entry<String, String> headerEntry : pageResponse.getHeaders().entrySet()) {
- headers.put(headerEntry.getKey(), headerEntry.getValue());
+ for (Header header : pageResponse.getHeaders()) {
+ for (String value : header) {
+ headers.put(header.getName(), value);
+ }
}
}
- data = pageRequest.getToadletContext().getBucketFactory().makeBucket(-1);
- if (pageResponse.getContent() != null) {
- try {
- BucketTools.copyFrom(data, pageResponse.getContent(), -1);
- } finally {
- Closer.close(pageResponse.getContent());
- }
- } else {
- /* get an OutputStream and close it immediately. */
- Closer.close(data.getOutputStream());
- }
- writeReply(pageRequest.getToadletContext(), pageResponse.getStatusCode(), pageResponse.getContentType(), pageResponse.getStatusText(), headers, data);
+ writeReply(pageRequest.getToadletContext(), pageResponse.getStatusCode(), pageResponse.getContentType(), pageResponse.getStatusText(), headers, pageBucket);
} catch (Throwable t1) {
writeInternalError(t1, pageRequest.getToadletContext());
} finally {
- Closer.close(data);
+ Closer.close(pageBucket);
}
}
/*
- * shortener - PageToadletFactory.java - Copyright © 2010 David Roden
+ * Sone - PageToadletFactory.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
package net.pterodactylus.sone.web.page;
+import net.pterodactylus.util.web.Page;
import freenet.client.HighLevelSimpleClient;
/**
* Factory that creates {@link PageToadlet}s using a given
* {@link HighLevelSimpleClient}.
- *
+ *
* @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
*/
public class PageToadletFactory {
/**
* Creates a new {@link PageToadlet} factory.
- *
+ *
* @param highLevelSimpleClient
* The client to use when creating the toadlets
* @param pathPrefix
/**
* Creates a {@link PageToadlet} that wraps the given page and does not
* appear in the node’s menu.
- *
+ *
* @param page
* The page to wrap
* @return The toadlet wrapped around the page
*/
- public PageToadlet createPageToadlet(Page page) {
+ public PageToadlet createPageToadlet(Page<FreenetRequest> page) {
return createPageToadlet(page, null);
}
/**
* Creates a {@link PageToadlet} that wraps the given page and appears in
* the node’s menu under the given name.
- *
+ *
* @param page
* The page to wrap
* @param menuName
* The name of the menu item
* @return The toadlet wrapped around the page
*/
- public PageToadlet createPageToadlet(Page page, String menuName) {
+ public PageToadlet createPageToadlet(Page<FreenetRequest> page, String menuName) {
return new PageToadlet(highLevelSimpleClient, menuName, page, pathPrefix);
}
+++ /dev/null
-/*
- * Sone - RedirectPage.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web.page;
-
-/**
- * Page implementation that redirects the user to another URL.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class RedirectPage implements Page {
-
- /** The original path. */
- private String originalPath;
-
- /** The path to redirect the browser to. */
- private String newPath;
-
- /**
- * Creates a new redirect page.
- *
- * @param originalPath
- * The original path
- * @param newPath
- * The path to redirect the browser to
- */
- public RedirectPage(String originalPath, String newPath) {
- this.originalPath = originalPath;
- this.newPath = newPath;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public String getPath() {
- return originalPath;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public Response handleRequest(Request request) {
- return new RedirectResponse(newPath);
- }
-
-}
+++ /dev/null
-/*
- * shortener - CSSPage.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
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web.page;
-
-import java.io.InputStream;
-
-/**
- * {@link Page} implementation that delivers static files from the class path.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class StaticPage implements Page {
-
- /** The prefix for {@link #getPath()}. */
- private final String pathPrefix;
-
- /** The path used as prefix when loading resources. */
- private final String resourcePathPrefix;
-
- /** The MIME type for the files this path contains. */
- private final String mimeType;
-
- /**
- * Creates a new CSS page.
- *
- * @param pathPrefix
- * The prefix for {@link #getPath()}
- * @param resourcePathPrefix
- * The path prefix when loading resources
- * @param mimeType
- * The MIME type of the files this path contains
- */
- public StaticPage(String pathPrefix, String resourcePathPrefix, String mimeType) {
- this.pathPrefix = pathPrefix;
- this.resourcePathPrefix = resourcePathPrefix;
- this.mimeType = mimeType;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public String getPath() {
- return pathPrefix;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public Response handleRequest(Request request) {
- String path = request.getUri().getPath();
- int lastSlash = path.lastIndexOf('/');
- String filename = path.substring(lastSlash + 1);
- InputStream fileInputStream = getClass().getResourceAsStream(resourcePathPrefix + filename);
- if (fileInputStream == null) {
- return new Response(404, "Not found.", null, "");
- }
- return new Response(200, "OK", mimeType, null, fileInputStream);
- }
-
-}
+++ /dev/null
-/*
- * Sone - StaticTemplatePage.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.web.page;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import net.pterodactylus.util.io.Closer;
-import net.pterodactylus.util.logging.Logging;
-import net.pterodactylus.util.template.Template;
-import net.pterodactylus.util.template.TemplateContext;
-import net.pterodactylus.util.template.TemplateContextFactory;
-
-/**
- * A template page is a single page that is created from a {@link Template} but
- * does not necessarily return HTML.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class TemplatePage implements Page {
-
- /** The logger. */
- private static final Logger logger = Logging.getLogger(TemplatePage.class);
-
- /** The path of this page. */
- private final String path;
-
- /** The content type of this page. */
- private final String contentType;
-
- /** The template context factory. */
- private final TemplateContextFactory templateContextFactory;
-
- /** The template to render. */
- private final Template template;
-
- /**
- * Creates a new template page.
- *
- * @param path
- * The path of the page
- * @param contentType
- * The content type of the page
- * @param templateContextFactory
- * The template context factory
- * @param template
- * The template to render
- */
- public TemplatePage(String path, String contentType, TemplateContextFactory templateContextFactory, Template template) {
- this.path = path;
- this.contentType = contentType;
- this.templateContextFactory = templateContextFactory;
- this.template = template;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public String getPath() {
- return path;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public Response handleRequest(Request request) {
- ByteArrayOutputStream responseOutputStream = new ByteArrayOutputStream();
- OutputStreamWriter responseWriter = null;
- try {
- responseWriter = new OutputStreamWriter(responseOutputStream, "UTF-8");
- TemplateContext templateContext = templateContextFactory.createTemplateContext();
- templateContext.set("request", request);
- template.render(templateContext, responseWriter);
- } catch (IOException ioe1) {
- logger.log(Level.WARNING, "Could not render template for path “" + path + "”!", ioe1);
- } finally {
- Closer.close(responseWriter);
- Closer.close(responseOutputStream);
- }
- ByteArrayInputStream responseInputStream = new ByteArrayInputStream(responseOutputStream.toByteArray());
- /* no need to close a ByteArrayInputStream. */
- return new Response(200, "OK", contentType, null, responseInputStream);
- }
-
-}
Navigation.Menu.Item.Logout.Tooltip=Logs you out of the current Sone
Navigation.Menu.Item.Options.Name=Options
Navigation.Menu.Item.Options.Tooltip=Options for the Sone plugin
+Navigation.Menu.Item.Rescue.Name=Rescue
+Navigation.Menu.Item.Rescue.Tooltip=Rescue Sone
Navigation.Menu.Item.About.Name=About
Navigation.Menu.Item.About.Tooltip=Information about Sone
Page.Options.Section.SoneSpecificOptions.Title=Sone-specific Options
Page.Options.Section.SoneSpecificOptions.NotLoggedIn=These options are only available if you are {link}logged in{/link}.
Page.Options.Section.SoneSpecificOptions.LoggedIn=These options are only available while you are logged in and they are only valid for the Sone you are logged in as.
-Page.Options.Option.AutoFollow.Description=If a new Sone is discovered, follow it automatically.
+Page.Options.Option.AutoFollow.Description=If a new Sone is discovered, follow it automatically. Note that this will only follow Sones that are discovered after you activate this option!
+Page.Options.Option.EnableSoneInsertNotifications.Description=If enabled, this will display notifications every time your Sone is being inserted or finishes inserting.
Page.Options.Section.RuntimeOptions.Title=Runtime Behaviour
Page.Options.Option.InsertionDelay.Description=The number of seconds the Sone inserter waits after a modification of a Sone before it is being inserted.
Page.Options.Option.PostsPerPage.Description=The number of posts to display on a page before pagination controls are being shown.
+Page.Options.Option.CharactersPerPost.Description=The number of characters to display from a post before cutting it off and showing a link to expand it (-1 to disable).
+Page.Options.Option.RequireFullAccess.Description=Whether to deny access to Sone to any host that has not been granted full access.
Page.Options.Section.TrustOptions.Title=Trust Settings
Page.Options.Option.PositiveTrust.Description=The amount of positive trust you want to assign to other Sones by clicking the checkmark below a post or reply.
Page.Options.Option.NegativeTrust.Description=The amount of trust you want to assign to other Sones by clicking the red X below a post or reply. This value should be negative.
Page.Options.Option.TrustComment.Description=The comment that will be set in the web of trust for any trust you assign from Sone.
-Page.Options.Section.RescueOptions.Title=Rescue Settings
-Page.Options.Option.SoneRescueMode.Description=Try to rescue your Sones at the next start of the Sone plugin. This will read your all your old Sones from Freenet and ignore any disappearing postings and replies. You have to unlock your local Sones after they have been restored and you have to manually disable the rescue mode once you are satisfied with what has been restored!
+Page.Options.Section.FcpOptions.Title=FCP Interface Settings
+Page.Options.Option.FcpInterfaceActive.Description=Activate the FCP interface to allow other plugins and remote clients to access your Sone plugin.
+Page.Options.Option.FcpFullAccessRequired.Description=Require FCP connection from allowed hosts (see your {link}node’s configuration, section “FCP”{/link})
+Page.Options.Option.FcpFullAccessRequired.Value.No=No
+Page.Options.Option.FcpFullAccessRequired.Value.Writing=For Write Access
+Page.Options.Option.FcpFullAccessRequired.Value.Always=Always
Page.Options.Section.Cleaning.Title=Clean Up
Page.Options.Option.ClearOnNextRestart.Description=Resets the configuration of the Sone plugin at the next restart. Warning! {strong}This will destroy all of your Sones{/strong} so make sure you have backed up everyhing you still need! Also, you need to set the next option to true to actually do it.
Page.Options.Option.ReallyClearOnNextRestart.Description=This option needs to be set to “yes” if you really, {strong}really{/strong} want to clear the plugin configuration on the next restart.
+Page.Options.Warnings.ValueNotChanged=This option was not changed because the value you specified was not valid.
Page.Options.Button.Save=Save
Page.Login.Title=Login - Sone
Page.KnownSones.Title=Known Sones - Sone
Page.KnownSones.Page.Title=Known Sones
Page.KnownSones.Text.NoKnownSones=There are currently no known Sones.
+Page.KnownSones.Button.FollowAllSones=Follow all Sones
+Page.KnownSones.Button.UnfollowAllSones=Unfollow all Sones
Page.EditProfile.Title=Edit Profile - Sone
Page.EditProfile.Page.Title=Edit Profile
Page.ViewSone.Page.TitleWithoutSone=View unknown Sone
Page.ViewSone.NoSone.Description=There is currently no known Sone with the ID {sone}. If you were looking for a specific Sone, make sure that it is visible in your web of trust!
Page.ViewSone.UnknownSone.Description=This Sone has not yet been retrieved. Please check back in a short time.
+Page.ViewSone.UnknownSone.LinkToWebOfTrust=Even though the Sone is still unknown, its Web of Trust profile might already be available:
Page.ViewSone.WriteAMessage=You can write a message to this Sone here. Please note that everybody will be able to read this message!
Page.ViewSone.PostList.Title=Posts by {sone}
Page.ViewSone.PostList.Text.NoPostYet=This Sone has not yet posted anything.
Page.ViewSone.Profile.Title=Profile
Page.ViewSone.Profile.Label.Name=Name
Page.ViewSone.Profile.Label.Albums=Albums
-Page.ViewSone.Replies.Title=Replies to Posts
+Page.ViewSone.Profile.Name.WoTLink=web of trust profile
+Page.ViewSone.Replies.Title=Posts {sone} has replied to
Page.ViewPost.Title=View Post - Sone
Page.ViewPost.Page.Title=View Post by {sone}
Page.Search.Text.PostHits=The following posts match your search terms.
Page.Search.Text.NoHits=No Sones or posts matched your search terms.
+Page.Rescue.Title=Rescue Sone
+Page.Rescue.Page.Title=Rescue Sone “{0}”
+Page.Rescue.Text.Description=The Rescue Mode lets you restore previous versions of your Sone. This can be necessary if your configuration was lost.
+Page.Rescue.Text.Procedure=The Rescue Mode works by fetching the latest inserted edition of your Sone. If an edition was successfully fetched it will be loaded into your Sone, letting you control your posts, profile, and other settings (you could do that in a second browser tab or window). If the fetched edition is not the one you want to restore, instruct the Rescue Mode to fetch the next older edition below.
+Page.Rescue.Text.Fetching=The Sone Rescuer is currently fetching edition {0} of your Sone.
+Page.Rescue.Text.Fetched=The Sone Rescuer has downloaded edition {0} of your Sone. Please check your posts, replies, and profile. If you like what the current Sone contains, just unlock it.
+Page.Rescue.Text.FetchedLast=The Sone rescuer has downloaded the last available edition. If it did not manage to restore your Sone you are probably out of luck now.
+Page.Rescue.Text.NotFetched=The Sone Rescuer could not download edition {0} of your Sone. Please either try again with edition {0}, or try the next older edition.
+Page.Rescue.Label.NextEdition=Next edition:
+Page.Rescue.Button.Fetch=Fetch edition
+
Page.NoPermission.Title=Unauthorized Access - Sone
Page.NoPermission.Page.Title=Unauthorized Access
Page.NoPermission.Text.NoPermission=You tried to do something that you do not have sufficient authorization for. Please refrain from such actions in the future or we will be forced to take counter-measures!
View.Sone.Label.LastUpdate=Last update:
View.Sone.Text.UnknownDate=unknown
+View.Sone.Stats.Posts={0,number} {0,choice,0#posts|1#post|1<posts}
+View.Sone.Stats.Replies={0,number} {0,choice,0#replies|1#reply|1<replies}
View.Sone.Button.UnlockSone=unlock
View.Sone.Button.UnlockSone.Tooltip=Allow this Sone to be inserted now
View.Sone.Button.LockSone=lock
View.Sone.Status.Inserting=This Sone is currently being inserted.
View.Post.UnknownAuthor=(unknown)
+View.Post.WebOfTrustLink=web of trust profile
+View.Post.Permalink=link post
+View.Post.PermalinkAuthor=link author
View.Post.Bookmarks.PostIsBookmarked=Post is bookmarked, click to remove from bookmarks
View.Post.Bookmarks.PostIsNotBookmarked=Post is not bookmarked, click to bookmark
View.Post.DeleteLink=Delete
View.Post.UnlikeLink=Unlike
View.Post.ShowSource=Toggle Parser
View.Post.NotDownloaded=This post has not yet been downloaded, or it has been deleted.
+View.Post.ShowMore=show more
+View.Post.ShowLess=show less
View.UpdateStatus.Text.ChooseSenderIdentity=Choose the sender identity
View.Time.ADayAgo=about a day ago
View.Time.XDaysAgo=${day} days ago
View.Time.AWeekAgo=about a week ago
-View.Time.XWeeksAgo=${week} week ago
+View.Time.XWeeksAgo=${week} weeks ago
View.Time.AMonthAgo=about a month ago
View.Time.XMonthsAgo=${month} months ago
View.Time.AYearAgo=about a year ago
WebInterface.DefaultText.EditImage.Title=Image title
WebInterface.DefaultText.EditImage.Description=Image description
WebInterface.DefaultText.Option.PostsPerPage=Number of posts to show on a page
+WebInterface.DefaultText.Option.CharactersPerPost=Number of characters per post after which to cut the post off
WebInterface.DefaultText.Option.PositiveTrust=The positive trust to assign
WebInterface.DefaultText.Option.NegativeTrust=The negative trust to assign
WebInterface.DefaultText.Option.TrustComment=The comment to set in the web of trust
Notification.InsertingImages.Text=The following images are being inserted:
Notification.InsertedImages.Text=The following images have been inserted:
Notification.ImageInsertFailed.Text=The following images could not be inserted:
+Notification.Mention.ShortText=You have been mentioned.
+Notification.Mention.Text=You have been mentioned in the following posts:
+Notification.SoneInsert.Duration={0,number} {0,choice,0#seconds|1#second|1<seconds}
content: '★ ';
}
+#sone a.in-page-link:before {
+ content: '↓ ';
+}
+
#sone a img {
border: none;
}
white-space: pre-wrap;
}
+#sone #main.offline {
+ opacity: 0.5;
+}
+
+#sone #offline-marker {
+ display: none;
+ position: fixed;
+ top: 2em;
+ right: 2em;
+ width: 128px;
+ height: 128px;
+ background-image: url("../images/sone-offline.png");
+}
+
#sone #notification-area {
margin-top: 1em;
}
display: none;
}
+#sone #notification-area #local-post-notification, #sone #notification-area #local-reply-notification {
+ display: none;
+}
+
#sone #plugin-warning {
border: solid 0.5em red;
padding: 0.5em;
padding: 1ex 0px;
border-bottom: solid 1px #ccc;
clear: both;
+ position: relative;
}
#sone .post.new {
border-bottom: none;
}
+#sone .post .sone-menu {
+ position: absolute;
+ top: 0;
+ left: -1ex;
+ padding: 1ex 1ex;
+ margin: -1px -1px;
+ display: none;
+ background-color: rgb(255, 255, 224);
+ border: solid 1px rgb(0, 0, 0);
+ z-index: 1;
+}
+
+#sone .post .sone-menu .avatar {
+ position: absolute;
+ margin-right: 1ex;
+}
+
+#sone .post .sone-menu .inner-menu {
+ margin-left: 64px;
+ padding-left: 1ex;
+ min-height: 64px;
+}
+
+#sone .sone-menu .follow, #sone .sone-menu .unfollow {
+ cursor: pointer;
+}
+
#sone .post > .avatar {
position: absolute;
}
font-weight: bold;
}
-#sone .post .text, #sone .post .raw-text {
+#sone .post .author-wot-link {
+ font-size: 90%;
+}
+
+#sone .post .text, #sone .post .raw-text, #sone .post .short-text {
display: inline;
white-space: pre-wrap;
+ word-wrap: break-word;
}
-#sone .post .text.hidden, #sone .post .raw-text.hidden {
+#sone .post .text.hidden, #sone .post .raw-text.hidden, #sone .post .short-text.hidden {
display: none;
}
+#sone .post .expand-post-text:before, #sone .post .expand-reply-text:before {
+ content: "» ";
+}
+
+#sone .post .shrink-post-text:before, #sone .post .shrink-reply-text:before {
+ content: "« ";
+}
+
+#sone .post .shrink-post-text {
+ cursor: pointer;
+}
+
#sone .post .status-line {
margin-top: 0.5ex;
font-size: 85%;
display: inline;
}
+#sone .permalink {
+ display: inline;
+}
+
#sone .post .bookmarks {
display: inline;
color: rgb(28, 131, 191);
}
#sone .post .reply {
+ position: relative;
clear: both;
background-color: #f0f0ff;
- font-size: 85%;
margin: 1ex 0px;
padding: 1ex;
}
+#sone .post .reply .inner-part {
+ font-size: 85%;
+}
+
#sone .post .reply.new {
background-color: #ffffa0;
}
}
#sone .sone .profile-link {
- display: block;
+ display: inline;
+}
+
+#sone .sone .sone-stats {
+ display: inline;
}
#sone .sone .short-request-uri {
font-weight: bold;
color: red;
}
+
+#sone .warning {
+ color: red;
+ font-style: italic;
+}
+
+#sone #sort-options {
+ margin-bottom: 1em;
+}
--- /dev/null
+/*
+ * jQuery plugin: fieldSelection - v0.1.0 - last change: 2006-12-16
+ * (c) 2006 Alex Brem <alex@0xab.cd> - http://blog.0xab.cd
+ */
+
+(function() {
+
+ var fieldSelection = {
+
+ getSelection: function() {
+
+ var e = this.jquery ? this[0] : this;
+
+ return (
+
+ /* mozilla / dom 3.0 */
+ ('selectionStart' in e && function() {
+ var l = e.selectionEnd - e.selectionStart;
+ return { start: e.selectionStart, end: e.selectionEnd, length: l, text: e.value.substr(e.selectionStart, l) };
+ }) ||
+
+ /* exploder */
+ (document.selection && function() {
+
+ e.focus();
+
+ var r = document.selection.createRange();
+ if (r == null) {
+ return { start: 0, end: e.value.length, length: 0 }
+ }
+
+ var re = e.createTextRange();
+ var rc = re.duplicate();
+ re.moveToBookmark(r.getBookmark());
+ rc.setEndPoint('EndToStart', re);
+
+ return { start: rc.text.length, end: rc.text.length + r.text.length, length: r.text.length, text: r.text };
+ }) ||
+
+ /* browser not supported */
+ function() {
+ return { start: 0, end: e.value.length, length: 0 };
+ }
+
+ )();
+
+ },
+
+ replaceSelection: function() {
+
+ var e = this.jquery ? this[0] : this;
+ var text = arguments[0] || '';
+
+ return (
+
+ /* mozilla / dom 3.0 */
+ ('selectionStart' in e && function() {
+ e.value = e.value.substr(0, e.selectionStart) + text + e.value.substr(e.selectionEnd, e.value.length);
+ return this;
+ }) ||
+
+ /* exploder */
+ (document.selection && function() {
+ e.focus();
+ document.selection.createRange().text = text;
+ return this;
+ }) ||
+
+ /* browser not supported */
+ function() {
+ e.value += text;
+ return this;
+ }
+
+ )();
+
+ }
+
+ };
+
+ jQuery.each(fieldSelection, function(i) { jQuery.fn[i] = this; });
+
+})();
/* Sone JavaScript functions. */
-/* jQuery overrides. */
-oldGetJson = jQuery.prototype.getJSON;
-jQuery.prototype.getJSON = function(url, data, successCallback, errorCallback) {
- if (typeof errorCallback == "undefined") {
- return oldGetJson(url, data, successCallback);
- }
- if (jQuery.isFunction(data)) {
- errorCallback = successCallback;
- successCallback = data;
- data = null;
- }
- return jQuery.ajax({
- data: data,
- error: errorCallback,
- success: successCallback,
- url: url
- });
-}
-
-function isOnline() {
- return $("#sone").hasClass("online");
+function ajaxGet(url, data, successCallback, errorCallback) {
+ (function(url, data, successCallback, errorCallback) {
+ $.ajax({"type": "GET", "url": url, "data": data, "dataType": "json", "success": function(data, textStatus, xmlHttpRequest) {
+ ajaxSuccess();
+ if (typeof successCallback != "undefined") {
+ successCallback(data, textStatus);
+ }
+ }, "error": function(xmlHttpRequest, textStatus, errorThrown) {
+ if (xmlHttpRequest.status == 403) {
+ notLoggedIn = true;
+ }
+ if (typeof errorCallback != "undefined") {
+ errorCallback();
+ } else {
+ ajaxError();
+ }
+ }});
+ })(url, data, successCallback, errorCallback);
}
function registerInputTextareaSwap(inputElement, defaultText, inputFieldName, optional, dontUseTextarea) {
});
})(replyElement);
textArea = replyElement.find("input.reply-input").focus().data("textarea");
- textArea.val(textArea.val() + "@sone://" + author + " ");
+ if (author != getCurrentSoneId()) {
+ textArea.val(textArea.val() + "@sone://" + author + " ");
+ }
});
return commentElement;
})(postId, author);
callback(translations[key]);
return;
}
- $.getJSON("getTranslation.ajax", {"key": key}, function(data, textStatus) {
+ ajaxGet("getTranslation.ajax", {"key": key}, function(data, textStatus) {
if ((data != null) && data.success) {
translations[key] = data.value;
callback(data.value);
}
- }, function(xmlHttpRequest, textStatus, error) {
- /* ignore error. */
});
}
* @param lastUpdated
* The date and time of the last update (formatted for display)
*/
-function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated) {
+function updateSoneStatus(soneId, name, status, modified, locked, lastUpdated, lastUpdatedText) {
$("#sone .sone." + filterSoneId(soneId)).
toggleClass("unknown", status == "unknown").
toggleClass("idle", status == "idle").
$("#sone .sone." + filterSoneId(soneId) + " .lock").toggleClass("hidden", locked);
$("#sone .sone." + filterSoneId(soneId) + " .unlock").toggleClass("hidden", !locked);
if (lastUpdated != null) {
- $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").text(lastUpdated);
+ $("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").attr("title", lastUpdated).text(lastUpdatedText);
} else {
getTranslation("View.Sone.Text.UnknownDate", function(unknown) {
$("#sone .sone." + filterSoneId(soneId) + " .last-update span.time").text(unknown);
*/
function enhanceDeletePostButton(button, postId, text) {
enhanceDeleteButton(button, text, function() {
- $.getJSON("deletePost.ajax", { "post": postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+ ajaxGet("deletePost.ajax", { "post": postId, "formPassword": getFormPassword() }, function(data, textStatus) {
if (data == null) {
return;
}
*/
function enhanceDeleteReplyButton(button, replyId, text) {
enhanceDeleteButton(button, text, function() {
- $.getJSON("deleteReply.ajax", { "reply": replyId, "formPassword": $("#sone #formPassword").text() }, function(data, textStatus) {
+ ajaxGet("deleteReply.ajax", { "reply": replyId, "formPassword": $("#sone #formPassword").text() }, function(data, textStatus) {
if (data == null) {
return;
}
*/
function getSone(soneId) {
return $("#sone .sone").filter(function(index) {
- return $(".id").text() == soneId;
+ return $(".id", this).text() == soneId;
});
}
}
/**
+ * Returns the ID of the sone of the context menu that contains the given
+ * element.
+ *
+ * @param element
+ * The element within a context menu to get the Sone ID for
+ * @return The Sone ID
+ */
+function getMenuSone(element) {
+ return $(element).closest(".sone-menu").find(".sone-menu-id").text();
+}
+
+/**
* Generates a list of Sones by concatening the names of the given sones with a
* new line character (“\n”).
*
return $(notificationElement).attr("id");
}
+/**
+ * Returns the time the notification was last updated.
+ *
+ * @param notificationElement
+ * The notification element
+ * @returns The last update time of the notification
+ */
+function getNotificationLastUpdatedTime(notificationElement) {
+ return $(notificationElement).attr("lastUpdatedTime");
+}
+
function likePost(postId) {
- $.getJSON("like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+ ajaxGet("like.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
if ((data == null) || !data.success) {
return;
}
}
function unlikePost(postId) {
- $.getJSON("unlike.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
+ ajaxGet("unlike.ajax", { "type": "post", "post" : postId, "formPassword": getFormPassword() }, function(data, textStatus) {
if ((data == null) || !data.success) {
return;
}
}
function updatePostLikes(postId) {
- $.getJSON("getLikes.ajax", { "type": "post", "post": postId }, function(data, textStatus) {
+ ajaxGet("getLikes.ajax", { "type": "post", "post": postId }, function(data, textStatus) {
if ((data != null) && data.success) {
- $("#sone .post#" + postId + " > .inner-part > .status-line .likes").toggleClass("hidden", data.likes == 0)
+ $("#sone .post#" + postId + " > .inner-part > .status-line .likes").toggleClass("hidden", data.likes == 0);
$("#sone .post#" + postId + " > .inner-part > .status-line .likes span.like-count").text(data.likes);
$("#sone .post#" + postId + " > .inner-part > .status-line .likes > span").attr("title", generateSoneList(data.sones));
}
}
function likeReply(replyId) {
- $.getJSON("like.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
+ ajaxGet("like.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
if ((data == null) || !data.success) {
return;
}
}
function unlikeReply(replyId) {
- $.getJSON("unlike.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
+ ajaxGet("unlike.ajax", { "type": "reply", "reply" : replyId, "formPassword": getFormPassword() }, function(data, textStatus) {
if ((data == null) || !data.success) {
return;
}
* The ID of the Sone to trust
*/
function trustSone(soneId) {
- $.getJSON("trustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+ ajaxGet("trustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
if ((data != null) && data.success) {
updateTrustControls(soneId, data.trustValue);
}
* The ID of the Sone to distrust
*/
function distrustSone(soneId) {
- $.getJSON("distrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+ ajaxGet("distrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
if ((data != null) && data.success) {
updateTrustControls(soneId, data.trustValue);
}
* The ID of the Sone to untrust
*/
function untrustSone(soneId) {
- $.getJSON("untrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
+ ajaxGet("untrustSone.ajax", { "formPassword" : getFormPassword(), "sone" : soneId }, function(data, textStatus) {
if ((data != null) && data.success) {
updateTrustControls(soneId, data.trustValue);
}
*/
function bookmarkPost(postId) {
(function(postId) {
- $.getJSON("bookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+ ajaxGet("bookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
if ((data != null) && data.success) {
getPost(postId).find(".bookmark").toggleClass("hidden", true);
getPost(postId).find(".unbookmark").toggleClass("hidden", false);
* The ID of the post to unbookmark
*/
function unbookmarkPost(postId) {
- $.getJSON("unbookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
+ ajaxGet("unbookmark.ajax", {"formPassword": getFormPassword(), "type": "post", "post": postId}, function(data, textStatus) {
if ((data != null) && data.success) {
getPost(postId).find(".bookmark").toggleClass("hidden", false);
getPost(postId).find(".unbookmark").toggleClass("hidden", true);
}
function updateReplyLikes(replyId) {
- $.getJSON("getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
+ ajaxGet("getLikes.ajax", { "type": "reply", "reply": replyId }, function(data, textStatus) {
if ((data != null) && data.success) {
- $("#sone .reply#" + replyId + " .status-line .likes").toggleClass("hidden", data.likes == 0)
+ $("#sone .reply#" + replyId + " .status-line .likes").toggleClass("hidden", data.likes == 0);
$("#sone .reply#" + replyId + " .status-line .likes span.like-count").text(data.likes);
$("#sone .reply#" + replyId + " .status-line .likes > span").attr("title", generateSoneList(data.sones));
}
* parameters: success, error, replyId)
*/
function postReply(sender, postId, text, callbackFunction) {
- $.getJSON("createReply.ajax", { "formPassword" : getFormPassword(), "sender": sender, "post" : postId, "text": text }, function(data, textStatus) {
+ ajaxGet("createReply.ajax", { "formPassword" : getFormPassword(), "sender": sender, "post" : postId, "text": text }, function(data, textStatus) {
if (data == null) {
/* TODO - show error */
return;
*/
$(".follow", soneElement).submit(function() {
var followElement = this;
- $.getJSON("followSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
+ ajaxGet("followSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
$(followElement).addClass("hidden");
$(followElement).parent().find(".unfollow").removeClass("hidden");
});
});
$(".unfollow", soneElement).submit(function() {
var unfollowElement = this;
- $.getJSON("unfollowSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
+ ajaxGet("unfollowSone.ajax", { "sone": getSoneId(this), "formPassword": getFormPassword() }, function() {
$(unfollowElement).addClass("hidden");
$(unfollowElement).parent().find(".follow").removeClass("hidden");
});
});
$(".lock", soneElement).submit(function() {
var lockElement = this;
- $.getJSON("lockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
+ ajaxGet("lockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
$(lockElement).addClass("hidden");
$(lockElement).parent().find(".unlock").removeClass("hidden");
});
});
$(".unlock", soneElement).submit(function() {
var unlockElement = this;
- $.getJSON("unlockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
+ ajaxGet("unlockSone.ajax", { "sone" : getSoneId(this), "formPassword" : getFormPassword() }, function() {
$(unlockElement).addClass("hidden");
$(unlockElement).parent().find(".lock").removeClass("hidden");
});
/* convert “show source” link into javascript function. */
$(postElement).find(".show-source").each(function() {
$("a", this).click(function() {
+ post = getPostElement(this);
+ rawPostText = $(".post-text.raw-text", post);
+ rawPostText.toggleClass("hidden");
+ if (rawPostText.hasClass("hidden")) {
+ $(".post-text.short-text", post).removeClass("hidden");
+ $(".post-text.text", post).addClass("hidden");
+ $(".expand-post-text", post).removeClass("hidden");
+ $(".shrink-post-text", post).addClass("hidden");
+ } else {
+ $(".post-text.short-text", post).addClass("hidden");
+ $(".post-text.text", post).addClass("hidden");
+ $(".expand-post-text", post).addClass("hidden");
+ $(".shrink-post-text", post).addClass("hidden");
+ }
+ return false;
+ });
+ });
+
+ /* convert “show more” link into javascript function. */
+ $(postElement).find(".expand-post-text").each(function() {
+ $(this).click(function() {
$(".post-text.text", getPostElement(this)).toggleClass("hidden");
- $(".post-text.raw-text", getPostElement(this)).toggleClass("hidden");
+ $(".post-text.short-text", getPostElement(this)).toggleClass("hidden");
+ $(".expand-post-text", getPostElement(this)).toggleClass("hidden");
+ $(".shrink-post-text", getPostElement(this)).toggleClass("hidden");
return false;
});
});
+ $(postElement).find(".shrink-post-text").each(function() {
+ $(this).click(function() {
+ $(".post-text.text", getPostElement(this)).toggleClass("hidden");
+ $(".post-text.short-text", getPostElement(this)).toggleClass("hidden");
+ $(".expand-post-text", getPostElement(this)).toggleClass("hidden");
+ $(".shrink-post-text", getPostElement(this)).toggleClass("hidden");
+ return false;
+ })
+ });
+
+ /* ajaxify author/post links */
+ $(".post-status-line .permalink a", postElement).click(function() {
+ if (!$(".create-reply", postElement).hasClass("hidden")) {
+ textArea = $("input.reply-input", postElement).focus().data("textarea");
+ $(textArea).replaceSelection($(this).attr("href"));
+ }
+ return false;
+ });
/* add “comment” link. */
- addCommentLink(getPostId(postElement), getPostAuthor(postElement), postElement, $(postElement).find(".post-status-line .time"));
+ addCommentLink(getPostId(postElement), getPostAuthor(postElement), postElement, $(postElement).find(".post-status-line .permalink-author"));
/* process all replies. */
replyIds = [];
/* hide reply input field. */
$(postElement).find(".create-reply").addClass("hidden");
+
+ /* show Sone menu when hovering over the avatar. */
+ $(postElement).find(".post-avatar").mouseover(function() {
+ $(".sone-menu:visible").fadeOut();
+ $(".sone-post-menu", postElement).mouseleave(function() {
+ $(this).fadeOut();
+ }).fadeIn();
+ return false;
+ });
+ (function(postElement) {
+ var soneId = $(".sone-menu-id", postElement).text();
+ $(".sone-post-menu .follow", postElement).click(function() {
+ var followElement = this;
+ ajaxGet("followSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
+ $(followElement).addClass("hidden");
+ $(followElement).parent().find(".unfollow").removeClass("hidden");
+ $("#sone .sone-menu").each(function() {
+ if (getMenuSone(this) == soneId) {
+ $(".follow", this).toggleClass("hidden", true);
+ $(".unfollow", this).toggleClass("hidden", false);
+ }
+ });
+ });
+ return false;
+ });
+ $(".sone-post-menu .unfollow", postElement).click(function() {
+ var unfollowElement = this;
+ ajaxGet("unfollowSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
+ $(unfollowElement).addClass("hidden");
+ $(unfollowElement).parent().find(".follow").removeClass("hidden");
+ $("#sone .sone-menu").each(function() {
+ if (getMenuSone(this) == soneId) {
+ $(".follow", this).toggleClass("hidden", false);
+ $(".unfollow", this).toggleClass("hidden", true);
+ }
+ });
+ });
+ return false;
+ });
+ })(postElement);
}
/**
});
});
})(replyElement);
- addCommentLink(getPostId(replyElement), getReplyAuthor(replyElement), replyElement, $(replyElement).find(".reply-status-line .time"));
+
+ /* ajaxify author links */
+ $(".reply-status-line .permalink a", replyElement).click(function() {
+ if (!$(".create-reply", getPostElement(replyElement)).hasClass("hidden")) {
+ textArea = $("input.reply-input", getPostElement(replyElement)).focus().data("textarea");
+ $(textArea).replaceSelection($(this).attr("href"));
+ }
+ return false;
+ });
+
+ addCommentLink(getPostId(replyElement), getReplyAuthor(replyElement), replyElement, $(replyElement).find(".reply-status-line .permalink-author"));
/* convert “show source” link into javascript function. */
$(replyElement).find(".show-reply-source").each(function() {
$("a", this).click(function() {
+ reply = getReplyElement(this);
+ rawReplyText = $(".reply-text.raw-text", reply);
+ rawReplyText.toggleClass("hidden");
+ if (rawReplyText.hasClass("hidden")) {
+ $(".reply-text.short-text", reply).removeClass("hidden");
+ $(".reply-text.text", reply).addClass("hidden");
+ $(".expand-reply-text", reply).removeClass("hidden");
+ $(".shrink-reply-text", reply).addClass("hidden");
+ } else {
+ $(".reply-text.short-text", reply).addClass("hidden");
+ $(".reply-text.text", reply).addClass("hidden");
+ $(".expand-reply-text", reply).addClass("hidden");
+ $(".shrink-reply-text", reply).addClass("hidden");
+ }
+ return false;
+ });
+ });
+
+ /* convert “show more” link into javascript function. */
+ $(replyElement).find(".expand-reply-text").each(function() {
+ $(this).click(function() {
+ $(".reply-text.text", getReplyElement(this)).toggleClass("hidden");
+ $(".reply-text.short-text", getReplyElement(this)).toggleClass("hidden");
+ $(".expand-reply-text", getReplyElement(this)).toggleClass("hidden");
+ $(".shrink-reply-text", getReplyElement(this)).toggleClass("hidden");
+ return false;
+ });
+ });
+ $(replyElement).find(".shrink-reply-text").each(function() {
+ $(this).click(function() {
$(".reply-text.text", getReplyElement(this)).toggleClass("hidden");
- $(".reply-text.raw-text", getReplyElement(this)).toggleClass("hidden");
+ $(".reply-text.short-text", getReplyElement(this)).toggleClass("hidden");
+ $(".expand-reply-text", getReplyElement(this)).toggleClass("hidden");
+ $(".shrink-reply-text", getReplyElement(this)).toggleClass("hidden");
return false;
});
});
untrustSone(getReplyAuthor(this));
return false;
});
+
+ /* show Sone menu when hovering over the avatar. */
+ $(replyElement).find(".reply-avatar").mouseover(function() {
+ $(".sone-menu:visible").fadeOut();
+ $(".sone-reply-menu", replyElement).mouseleave(function() {
+ $(this).fadeOut();
+ }).fadeIn();
+ return false;
+ });
+ (function(replyElement) {
+ var soneId = $(".sone-menu-id", replyElement).text();
+ $(".sone-menu .follow", replyElement).click(function() {
+ var followElement = this;
+ ajaxGet("followSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
+ $(followElement).addClass("hidden");
+ $(followElement).parent().find(".unfollow").removeClass("hidden");
+ $("#sone .sone-menu").each(function() {
+ if (getMenuSone(this) == soneId) {
+ $(".follow", this).toggleClass("hidden", true);
+ $(".unfollow", this).toggleClass("hidden", false);
+ }
+ });
+ });
+ return false;
+ });
+ $(".sone-menu .unfollow", replyElement).click(function() {
+ var unfollowElement = this;
+ ajaxGet("unfollowSone.ajax", { "sone": soneId, "formPassword": getFormPassword() }, function() {
+ $(unfollowElement).addClass("hidden");
+ $(unfollowElement).parent().find(".follow").removeClass("hidden");
+ $("#sone .sone-menu").each(function() {
+ if (getMenuSone(this) == soneId) {
+ $(".follow", this).toggleClass("hidden", false);
+ $(".unfollow", this).toggleClass("hidden", true);
+ }
+ });
+ });
+ return false;
+ });
+ })(replyElement);
}
/**
notification.find(".text").addClass("hidden");
}
notification.find("form.mark-as-read button").click(function() {
- $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": $(":input[name=type]", this.form).val(), "id": $(":input[name=id]", this.form).val()});
+ allIds = $(":input[name=id]", this.form).val().split(" ");
+ for (index = 0; index < allIds.length; index += 16) {
+ ids = allIds.slice(index, index + 16).join(" ");
+ ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": $(":input[name=type]", this.form).val(), "id": ids});
+ }
});
notification.find("a[class^='link-']").each(function() {
linkElement = $(this);
if (linkElement.is("[href^='viewPost']")) {
id = linkElement.attr("class").substr(5);
if (hasPost(id)) {
- linkElement.attr("href", "#post-" + id);
+ linkElement.attr("href", "#post-" + id).addClass("in-page-link");
}
}
});
notification.find("form.dismiss button").click(function() {
- $.getJSON("dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.attr("id") }, function(data, textStatus) {
+ ajaxGet("dismissNotification.ajax", { "formPassword" : getFormPassword(), "notification" : notification.attr("id") }, function(data, textStatus) {
/* dismiss in case of error, too. */
notification.slideUp();
}, function(xmlHttpRequest, textStatus, error) {
if (getNotificationId(oldNotification) != "new-sone-notification") {
return;
}
- oldIds = getElementIds(oldNotification, ".sone-id");
- newIds = getElementIds(newNotification, ".sone-id");
+ oldIds = getElementIds(oldNotification, ".new-sone-id");
+ newIds = getElementIds(newNotification, ".new-sone-id");
$.each(oldIds, function(index, value) {
if ($.inArray(value, newIds) == -1) {
markSoneAsKnown(getSone(value), true);
* The new notification element
*/
function checkForRemovedReplies(oldNotification, newNotification) {
- if (getNotificationId(oldNotification) != "new-replies-notification") {
+ if (getNotificationId(oldNotification) != "new-reply-notification") {
return;
}
oldIds = getElementIds(oldNotification, ".reply-id");
}
function getStatus() {
- $.getJSON("getStatus.ajax", {"loadAllSones": isKnownSonesPage()}, function(data, textStatus) {
+ ajaxGet("getStatus.ajax", isViewSonePage() ? {"soneIds": getShownSoneId() } : {"loadAllSones": isKnownSonesPage()}, function(data, textStatus) {
if ((data != null) && data.success) {
/* process Sone information. */
$.each(data.sones, function(index, value) {
- updateSoneStatus(value.id, value.name, value.status, value.modified, value.locked, value.lastUpdatedUnknown ? null : value.lastUpdated);
+ updateSoneStatus(value.id, value.name, value.status, value.modified, value.locked, value.lastUpdatedUnknown ? null : value.lastUpdated, value.lastUpdatedText);
});
+ notLoggedIn = !data.loggedIn;
+ if (!notLoggedIn) {
+ showOfflineMarker(!online);
+ }
/* search for removed notifications. */
$("#sone #notification-area .notification").each(function() {
notificationId = $(this).attr("id");
});
if (!foundNotification) {
if (notificationId == "new-sone-notification") {
- $(".sone-id", this).each(function(index, element) {
+ $(".new-sone-id", this).each(function(index, element) {
soneId = $(this).text();
markSoneAsKnown(getSone(soneId), true);
});
postId = $(this).text();
markPostAsKnown(getPost(postId), true);
});
- } else if (notificationId == "new-replies-notification") {
+ } else if (notificationId == "new-reply-notification") {
$(".reply-id", this).each(function(index, element) {
replyId = $(this).text();
markReplyAsKnown(getReply(replyId), true);
}
});
/* process notifications. */
+ notificationIds = [];
$.each(data.notifications, function(index, value) {
oldNotification = getNotification(value.id);
- notification = ajaxifyNotification(createNotification(value.id, value.text, value.dismissable)).hide();
- if (oldNotification.length != 0) {
- if ((oldNotification.find(".short-text").length > 0) && (notification.find(".short-text").length > 0)) {
- opened = oldNotification.is(":visible") && oldNotification.find(".short-text").hasClass("hidden");
- notification.find(".short-text").toggleClass("hidden", opened);
- notification.find(".text").toggleClass("hidden", !opened);
- }
- checkForRemovedSones(oldNotification, notification);
- checkForRemovedPosts(oldNotification, notification);
- checkForRemovedReplies(oldNotification, notification);
- oldNotification.replaceWith(notification.show());
- } else {
- $("#sone #notification-area").append(notification);
- notification.slideDown();
- setActivity();
+ if ((oldNotification.length == 0) || (value.lastUpdatedTime > getNotificationLastUpdatedTime(oldNotification))) {
+ notificationIds.push(value.id);
}
});
+ if (notificationIds.length > 0) {
+ loadNotifications(notificationIds);
+ }
/* process new posts. */
$.each(data.newPosts, function(index, value) {
loadNewPost(value.id, value.sone, value.recipient, value.time);
/* data.success was false, wait 30 seconds. */
setTimeout(getStatus, 30000);
}
- }, function(xmlHttpRequest, textStatus, error) {
- /* something really bad happend, wait a minute. */
- setTimeout(getStatus, 60000);
- })
+ }, function() {
+ statusRequestQueued = false;
+ ajaxError();
+ });
+}
+
+/**
+ * Requests multiple notifications from Sone and displays them.
+ *
+ * @param notificationIds
+ * Array of IDs of the notifications to load
+ */
+function loadNotifications(notificationIds) {
+ ajaxGet("getNotification.ajax", {"notifications": notificationIds.join(",")}, function(data, textStatus) {
+ if (!data || !data.success) {
+ // TODO - show error
+ return;
+ }
+ $.each(data.notifications, function(index, value) {
+ oldNotification = getNotification(value.id);
+ notification = ajaxifyNotification(createNotification(value.id, value.lastUpdatedTime, value.text, value.dismissable)).hide();
+ if (oldNotification.length != 0) {
+ if ((oldNotification.find(".short-text").length > 0) && (notification.find(".short-text").length > 0)) {
+ opened = oldNotification.is(":visible") && oldNotification.find(".short-text").hasClass("hidden");
+ notification.find(".short-text").toggleClass("hidden", opened);
+ notification.find(".text").toggleClass("hidden", !opened);
+ }
+ checkForRemovedSones(oldNotification, notification);
+ checkForRemovedPosts(oldNotification, notification);
+ checkForRemovedReplies(oldNotification, notification);
+ oldNotification.replaceWith(notification.show());
+ } else {
+ $("#sone #notification-area").append(notification);
+ if (value.id.substring(0, 5) != "local") {
+ notification.slideDown();
+ setActivity();
+ }
+ }
+ });
+ });
}
/**
}
/**
+ * Returns the current page of the selected pagination. If no pagination can be
+ * found with the given selector, {@code 1} is returned.
+ *
+ * @param paginationSelector
+ * The pagination selector
+ * @returns The current page of the pagination
+ */
+function getPage(paginationSelector) {
+ pagination = $(paginationSelector);
+ if (pagination.length > 0) {
+ return $(".current-page", paginationSelector).text();
+ }
+ return 1;
+}
+
+/**
* Returns whether the current page is a “view Sone” page.
*
* @returns {Boolean} <code>true</code> if the current page is a “view Sone”
if (hasPost(postId)) {
return;
}
- if (!isIndexPage()) {
+ if (!isIndexPage() || (getPage(".pagination-index") > 1)) {
if (!isViewPostPage() || (getShownPostId() != postId)) {
- if (!isViewSonePage() || ((getShownSoneId() != soneId) && (getShownSoneId() != recipientId))) {
+ if (!isViewSonePage() || ((getShownSoneId() != soneId) && (getShownSoneId() != recipientId)) || (getPage(".post-navigation") > 1)) {
return;
}
}
if (getPostTime($("#sone .post").last()) > time) {
return;
}
- $.getJSON("getPost.ajax", { "post" : postId }, function(data, textStatus) {
+ ajaxGet("getPost.ajax", { "post" : postId }, function(data, textStatus) {
if ((data != null) && data.success) {
if (hasPost(data.post.id)) {
return;
}
- if (!isIndexPage() && !(isViewSonePage() && ((getShownSoneId() == data.post.sone) || (getShownSoneId() == data.post.recipient)))) {
+ if ((!isIndexPage() || (getPage(".pagination-index") > 1)) && !(isViewSonePage() && ((getShownSoneId() == data.post.sone) || (getShownSoneId() == data.post.recipient) || (getPage(".post-navigation") > 1)))) {
return;
}
var firstOlderPost = null;
}
});
newPost = $(data.post.html).addClass("hidden");
+ if ($(".post-author-local", newPost).text() == "true") {
+ newPost.removeClass("new");
+ }
if (firstOlderPost != null) {
newPost.insertBefore(firstOlderPost);
}
if (!hasPost(postId)) {
return;
}
- $.getJSON("getReply.ajax", { "reply": replyId }, function(data, textStatus) {
+ ajaxGet("getReply.ajax", { "reply": replyId }, function(data, textStatus) {
/* find post. */
if ((data != null) && data.success) {
if (hasReply(data.reply.id)) {
}
});
newReply = $(data.reply.html).addClass("hidden");
+ if ($(".reply-author-local", newReply).text() == "true") {
+ newReply.removeClass("new");
+ (function(newReply) {
+ setTimeout(function() {
+ markReplyAsKnown(newReply, false);
+ }, 5000);
+ })(newReply);
+ }
if (firstNewerReply != null) {
newReply.insertBefore(firstNewerReply);
} else {
* request
*/
function markSoneAsKnown(soneElement, skipRequest) {
- if ($(".new", soneElement).length > 0) {
- if ((typeof skipRequest != "undefined") && !skipRequest) {
- $.getJSON("maskAsKnown.ajax", {"formPassword": getFormPassword(), "type": "sone", "id": getSoneId(soneElement)}, function(data, textStatus) {
- $(soneElement).removeClass("new");
- });
+ if ($(soneElement).is(".new")) {
+ $(soneElement).removeClass("new");
+ if ((typeof skipRequest == "undefined") || !skipRequest) {
+ ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "sone", "id": getSoneId(soneElement)});
}
}
}
function markPostAsKnown(postElements, skipRequest) {
$(postElements).each(function() {
postElement = this;
- if ($(postElement).hasClass("new")) {
+ if ($(postElement).hasClass("new") || ((typeof skipRequest != "undefined"))) {
(function(postElement) {
$(postElement).removeClass("new");
- $(".click-to-show", postElement).removeClass("new");
if ((typeof skipRequest == "undefined") || !skipRequest) {
- $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "post", "id": getPostId(postElement)});
+ ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "post", "id": getPostId(postElement)});
}
})(postElement);
}
+ $(".click-to-show", postElement).removeClass("new");
});
markReplyAsKnown($(postElements).find(".reply"));
}
function markReplyAsKnown(replyElements, skipRequest) {
$(replyElements).each(function() {
replyElement = this;
- if ($(replyElement).hasClass("new")) {
+ if ($(replyElement).hasClass("new") || ((typeof skipRequest != "undefined"))) {
(function(replyElement) {
$(replyElement).removeClass("new");
if ((typeof skipRequest == "undefined") || !skipRequest) {
- $.getJSON("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "reply", "id": getReplyId(replyElement)});
+ ajaxGet("markAsKnown.ajax", {"formPassword": getFormPassword(), "type": "reply", "id": getReplyId(replyElement)});
}
})(replyElement);
}
* Comma-separated post IDs
*/
function updatePostTimes(postIds) {
- $.getJSON("getTimes.ajax", { "posts" : postIds }, function(data, textStatus) {
+ ajaxGet("getTimes.ajax", { "posts" : postIds }, function(data, textStatus) {
if ((data != null) && data.success) {
$.each(data.postTimes, function(index, value) {
updatePostTime(index, value.timeText, value.refreshTime, value.tooltip);
* Comma-separated post IDs
*/
function updateReplyTimes(replyIds) {
- $.getJSON("getTimes.ajax", { "replies" : replyIds }, function(data, textStatus) {
+ ajaxGet("getTimes.ajax", { "replies" : replyIds }, function(data, textStatus) {
if ((data != null) && data.success) {
$.each(data.replyTimes, function(index, value) {
updateReplyTime(index, value.timeText, value.refreshTime, value.tooltip);
* <code>true</code> if the notification can be dismissed by the
* user
*/
-function createNotification(id, text, dismissable) {
- notification = $("<div></div>").addClass("notification").attr("id", id);
+function createNotification(id, lastUpdatedTime, text, dismissable) {
+ notification = $("<div></div>").addClass("notification").attr("id", id).attr("lastUpdatedTime", lastUpdatedTime);
if (dismissable) {
- dismissForm = $("#sone #notification-area #notification-dismiss-template").clone().removeClass("hidden").removeAttr("id")
+ dismissForm = $("#sone #notification-area #notification-dismiss-template").clone().removeClass("hidden").removeAttr("id");
dismissForm.find("input[name=notification]").val(id);
notification.append(dismissForm);
}
* The ID of the field to delete
*/
function deleteProfileField(fieldId) {
- $.getJSON("deleteProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId}, function(data, textStatus) {
+ ajaxGet("deleteProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId}, function(data, textStatus) {
if (data && data.success) {
$("#sone .profile-field#" + data.field.id).slideUp();
}
* Called when the renaming was successful
*/
function editProfileField(fieldId, newName, successFunction) {
- $.getJSON("editProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "name": newName}, function(data, textStatus) {
+ ajaxGet("editProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "name": newName}, function(data, textStatus) {
if (data && data.success) {
successFunction();
}
* Function to call on success
*/
function moveProfileField(fieldId, direction, successFunction) {
- $.getJSON("moveProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "direction": direction}, function(data, textStatus) {
+ ajaxGet("moveProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "direction": direction}, function(data, textStatus) {
if (data && data.success) {
successFunction();
}
moveProfileField(fieldId, "down", successFunction);
}
+var statusRequestQueued = true;
+
+/**
+ * Sets the status of the web interface as offline.
+ */
+function ajaxError() {
+ online = false;
+ showOfflineMarker(true);
+ if (!statusRequestQueued) {
+ setTimeout(getStatus, 5000);
+ statusRequestQueued = true;
+ }
+}
+
+/**
+ * Sets the status of the web interface as online.
+ */
+function ajaxSuccess() {
+ online = true;
+ showOfflineMarker(!online || (initiallyLoggedIn && notLoggedIn));
+}
+
+/**
+ * Shows or hides the offline marker.
+ *
+ * @param visible
+ * {@code true} to display the offline marker, {@code false} to hide
+ * it
+ */
+function showOfflineMarker(visible) {
+ /* jQuery documentation says toggle() works the other way around?! */
+ $("#sone #offline-marker").toggle(visible);
+ if (visible) {
+ $("#sone #main").addClass("offline");
+ } else {
+ $("#sone #main").removeClass("offline");
+ }
+}
+
//
// EVERYTHING BELOW HERE IS EXECUTED AFTER LOADING THE PAGE
//
var focus = true;
+var online = true;
+var initiallyLoggedIn = $("#sone #loggedIn").text() == "true";
+var notLoggedIn = !initiallyLoggedIn;
$(document).ready(function() {
}
sender = $(this).find(":input[name=sender]").val();
text = $(this).find(":input[name=text]:enabled").val();
- $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "sender": sender, "text": text }, function(data, textStatus) {
+ ajaxGet("createPost.ajax", { "formPassword": getFormPassword(), "sender": sender, "text": text }, function(data, textStatus) {
button.removeAttr("disabled");
});
$(this).find(":input[name=sender]").val(getCurrentSoneId());
$("#sone #post-message").submit(function() {
sender = $(this).find(":input[name=sender]").val();
text = $(this).find(":input[name=text]:enabled").val();
- $.getJSON("createPost.ajax", { "formPassword": getFormPassword(), "recipient": getShownSoneId(), "sender": sender, "text": text });
+ ajaxGet("createPost.ajax", { "formPassword": getFormPassword(), "recipient": getShownSoneId(), "sender": sender, "text": text });
$(this).find(":input[name=sender]").val(getCurrentSoneId());
$(this).find(":input[name=text]:enabled").val("").blur();
$(this).find(".sender").hide();
allReplies = $(this).find(".reply");
if (allReplies.length > 2) {
newHidden = false;
- for (replyIndex = 0; replyIndex < (allReplies.length - 2); ++replyIndex) {
+ for (replyIndex = 0; !newHidden && (replyIndex < (allReplies.length - 2)); ++replyIndex) {
$(allReplies[replyIndex]).addClass("hidden");
newHidden |= $(allReplies[replyIndex]).hasClass("new");
}
resetActivity();
}).blur(function() {
focus = false;
- })
+ });
});
-<div id="sone" class="<%ifnull ! currentSone>online<%else>offline<%/if>">
+<div id="sone">
<div id="formPassword"><% formPassword|html></div>
<div id="currentSoneId" class="hidden"><% currentSone.id|html></div>
+ <div id="loggedIn" class="hidden"><%ifnull !currentSone>true<%else>false<%/if></div>
<script src="javascript/jquery-1.4.2.js" language="javascript"></script>
<script src="javascript/jquery.url.js" language="javascript"></script>
+ <script src="javascript/jquery.fieldselection.js" language="javascript"></script>
<script src="javascript/sone.js" language="javascript"></script>
+ <div id="offline-marker"></div>
+
<div id="main">
<div id="notification-area">
<button type="submit"><%= Notification.Button.Dismiss|l10n|html></button>
</form>
- <%foreach webInterface.notifications.all notification>
- <div class="notification" id="<% notification.id|html>">
+ <%foreach notifications notification>
+ <div class="notification" id="<% notification.id|html>" lastUpdatedTime="<%notification.lastUpdatedTime|html>">
<%if notification.dismissable>
<form class="dismiss" action="dismissNotification.html" method="post">
<input type="hidden" name="formPassword" value="<% formPassword|html>" />
<%if pagination.necessary>
- <div class="navigation">
+ <div class="navigation <%paginationName|html>">
<div class="first"><%if ! pagination.first><a href="<% request|change nameKey=pageParameter value=0>">«</a><%else><span>«</span><%/if></div>
<div class="previous"><%if ! pagination.first><a href="<% request|change nameKey=pageParameter key=pagination.previousPage>">‹</a><%else><span>‹</span><%/if></div>
<div class="current-page"><% pagination.pageNumber></div>
--- /dev/null
+<div class="sone-menu <% class|css|html>">
+ <div class="sone-menu-id hidden"><%sone.id|html></div>
+ <img class="avatar" src="/WebOfTrust/GetIdenticon?identity=<%sone.id|html>&width=64&height=64" width="64" height="64" alt="Avatar Image" />
+ <div class="inner-menu">
+ <div>
+ <a class="author" href="viewSone.html?sone=<%sone.id|html>"><%sone.niceName|html></a>
+ <span class="author-wot-link">(<a href="/WebOfTrust/ShowIdentity?id=<%sone.id|html>"><% =View.Post.WebOfTrustLink|l10n|html></a>)</span>
+ </div>
+ <div><%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size></div>
+ <%if !sone.local>
+ <div>
+ <a class="follow<%if sone.friend> hidden<%/if>"><%= View.Sone.Button.FollowSone|l10n|html></a>
+ <a class="unfollow<%if !sone.friend> hidden<%/if>"><%= View.Sone.Button.UnfollowSone|l10n|html></a>
+ </div>
+ <%/if>
+ </div>
+</div>
<label for="sender"><%= Page.Index.Label.Sender|l10n|html></label>
<div class="sender">
<select name="sender" title="<%= View.UpdateStatus.Text.ChooseSenderIdentity|l10n|html>">
- <%foreach localSones localSone>
+ <%foreach localSones localSone|sort>
<option value="<% localSone.id|html>"<%if localSone.current> selected="selected"<%/if>><% localSone.niceName|html></option>
<%/foreach>
</select>
-<div id="<% post.id|html>" class="post<%if loop.last> last<%/if><%if post.new> new<%/if>">
+<div id="<% post.id|html>" class="post<%if loop.last> last<%/if><%if !post.sone.local><%if post.new> new<%/if><%/if>">
<a name="post-<% post.id|html>"></a>
<div class="post-time hidden"><% post.time|html></div>
<div class="post-author hidden"><% post.sone.id|html></div>
- <div class="avatar">
+ <div class="post-author-local hidden"><% post.sone.local></div>
+ <%include include/soneMenu.html class=="sone-post-menu" sone=post.sone>
+ <div class="avatar post-avatar" >
<%if post.loaded>
<img src="/WebOfTrust/GetIdenticon?identity=<% post.sone.id|html>&width=48&height=48" width="48" height="48" alt="Avatar Image" />
<%else>
<%/if>
<% post.text|html|store key=originalText text=true>
<% post.text|parse sone=post.sone|store key=parsedText text=true>
+ <% post.text|parse sone=post.sone length=core.preferences.charactersPerPost|store key=shortText text=true>
<div class="post-text raw-text<%if !raw> hidden<%/if>"><% originalText></div>
- <div class="post-text text<%if raw> hidden<%/if>"><% parsedText></div>
+ <div class="post-text text<%if raw> hidden<%/if><%if !shortText|match key=parsedText> hidden<%/if>"><% parsedText></div>
+ <div class="post-text short-text<%if raw> hidden<%/if><%if shortText|match key=parsedText> hidden<%/if>"><% shortText></div>
+ <%if !shortText|match key=parsedText><%if !raw><a class="expand-post-text" href="viewPost.html?post=<% post.id|html>&raw=true"><%= View.Post.ShowMore|l10n|html></a><%/if><%/if>
+ <%if !shortText|match key=parsedText><%if !raw><a class="shrink-post-text hidden"><%= View.Post.ShowLess|l10n|html></a><%/if><%/if>
</div>
<div class="post-status-line status-line<%if !post.loaded> hidden<%/if>">
<div class="bookmarks">
</div>
<span class='separator'>·</span>
<div class="time"><a href="viewPost.html?post=<% post.id|html>"><% post.time|date format="MMM d, yyyy, HH:mm:ss"></a></div>
+ <span class='separator'>·</span>
+ <div class="permalink permalink-post"><a href="post://<%post.id|html>">[<%= View.Post.Permalink|l10n|html>]</a></div>
+ <span class='separator'>·</span>
+ <div class="permalink permalink-author"><a href="sone://<%post.sone.id|html>">[<%= View.Post.PermalinkAuthor|l10n|html>]</a></div>
<%if ! originalText|match key=parsedText>
<span class='separator'>·</span>
<div class="show-source"><a href="viewPost.html?post=<% post.id|html>&raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
-<div id="<% reply.id|html>" class="reply<%if reply.new> new<%/if>">
+<div id="<% reply.id|html>" class="reply<%if !reply.sone.local><%if reply.new> new<%/if><%/if>">
<a name="reply-<% reply.id|html>"></a>
<div class="reply-time hidden"><% reply.time|html></div>
<div class="reply-author hidden"><% reply.sone.id|html></div>
- <div class="avatar">
+ <div class="reply-author-local hidden"><% reply.sone.local></div>
+ <%include include/soneMenu.html class=="sone-reply-menu" sone=reply.sone>
+ <div class="avatar reply-avatar">
<img src="/WebOfTrust/GetIdenticon?identity=<% reply.sone.id|html>&width=36&height=36" width="36" height="36" alt="Avatar Image" />
</div>
<div class="inner-part">
<div class="author profile-link"><a href="viewSone.html?sone=<% reply.sone.id|html>"><% reply.sone.niceName|html></a></div>
<% reply.text|html|store key=originalText text=true>
<% reply.text|parse sone=reply.sone|store key=parsedText text=true>
+ <% reply.text|parse sone=reply.sone length=core.preferences.charactersPerPost|store key=shortText text=true>
<div class="reply-text raw-text<%if !raw> hidden<%/if>"><% originalText></div>
- <div class="reply-text text<%if raw> hidden<%/if>"><% parsedText></div>
+ <div class="reply-text text<%if raw> hidden<%/if><%if !shortText|match key=parsedText> hidden<%/if>"><% parsedText></div>
+ <div class="reply-text short-text<%if raw> hidden<%/if><%if shortText|match key=parsedText> hidden<%/if>"><% shortText></div>
+ <%if !shortText|match key=parsedText><%if !raw><a class="expand-reply-text" href="viewPost.html?post=<% reply.post.id|html>&raw=true"><%= View.Post.ShowMore|l10n|html></a><%/if><%/if>
+ <%if !shortText|match key=parsedText><%if !raw><a class="shrink-reply-text hidden"><%= View.Post.ShowLess|l10n|html></a><%/if><%/if>
</div>
<div class="reply-status-line status-line">
<div class="time"><% reply.time|date format="MMM d, yyyy, HH:mm:ss"></div>
+ <span class='separator'>·</span>
+ <div class="permalink permalink-author"><a href="sone://<%reply.sone.id|html>">[<%= View.Post.PermalinkAuthor|l10n|html>]</a></div>
<%if ! originalText|match key=parsedText>
<span class='separator'>·</span>
<div class="show-reply-source"><a href="viewPost.html?post=<% post.id|html>&raw=<%if raw>false<%else>true<%/if>"><%= View.Post.ShowSource|l10n|html></a></div>
<div class="download-marker" title="<%= View.Sone.Status.Downloading|l10n|html>">⬊</div>
<div class="insert-marker" title="<%= View.Sone.Status.Inserting|l10n|html>">⬈</div>
<div class="idle-marker" title="<%= View.Sone.Status.Idle|l10n|html>">✔</div>
- <div class="last-update"><%= View.Sone.Label.LastUpdate|l10n|html> <span class="time"><% sone.time|unknown|date format="MMM d, yyyy, HH:mm:ss"></span></div>
- <div class="profile-link"><a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a></div>
+ <div class="last-update"><%= View.Sone.Label.LastUpdate|l10n|html> <span class="time" title="<% sone.time|unknown|date format="MMM d, yyyy, HH:mm:ss">"><%sone.lastUpdatedText|html></span></div>
+ <div>
+ <div class="profile-link"><a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a></div>
+ <div class="sone-stats">(<%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size>)</div>
+ </div>
<div class="short-request-uri"><% sone.requestUri|substring start=4 length=43|html></div>
<div class="hidden"><% sone.blacklisted></div>
<%if sone.local>
<div id="posts">
<%= page|store key=pageParameter>
- <%include include/pagination.html>
+ <%include include/pagination.html paginationName==pagination-index>
<%foreach pagination.items post>
<%include include/viewPost.html>
<%foreachelse>
<h1>Sone “<% currentSone.name|html>”</h1>
<p>
This page should not be viewed directly; it is merely a reminder
- that you should install the <i>Sone</i> plugin.
+ that you should install the <a href="/USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/<% currentEdition>/">Sone</a> plugin.
</p>
</body>
</html>
<%include include/head.html>
<div class="page-id hidden">known-sones</div>
+
+ <script language="javascript">
+
+ $(document).ready(function() {
+ $("select[name=sort]").change(function() {
+ value = $(this).val();
+ if ((value == "activity") || (value == "posts")) {
+ $("select[name=order]").val("desc");
+ } else if (value == "name") {
+ $("select[name=order]").val("asc");
+ }
+ });
+ $("#sort-options select").change(function() {
+ this.form.submit();
+ });
+ });
+
+ </script>
<h1><%= Page.KnownSones.Page.Title|l10n|html></h1>
+
+ <div id="sort-options">
+ <form action="knownSones.html" method="get">
+ <div>
+ Sort:
+ <select name="sort">
+ <option value="name"<%if sort|match value="name"> selected="selected"<%/if>>Name</option>
+ <option value="activity"<%if sort|match value="activity"> selected="selected"<%/if>>Last activity</option>
+ <option value="posts"<%if sort|match value="posts"> selected="selected"<%/if>>Number of posts</option>
+ </select>
+ <select name="order">
+ <option value="asc"<%if order|match value="asc"> selected="selected"<%/if>>Ascending</option>
+ <option value="desc"<%if order|match value="desc"> selected="selected"<%/if>>Descending</option>
+ </select>
+ </div>
+ <%ifnull !currentSone>
+ <div>
+ Followed Sones:
+ <select name="followedSones">
+ <option value="none"></option>
+ <option value="show-only"<%if followedSones|match value="show-only"> selected="selected"<%/if>>Show only followed Sones</option>
+ <option value="hide"<%if followedSones|match value="hide"> selected="selected"<%/if>>Hide followed Sones</option>
+ </select>
+ </div>
+ <%/if>
+ <div>
+ <button type="submit">Apply</button>
+ </div>
+ </form>
+ </div>
+
+ <div>
+ <form action="followSone.html" method="post">
+ <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+ <input type="hidden" name="returnPage" value="<%request.uri|html>" />
+ <input type="hidden" name="sone" value="<%foreach pagination.items sone><%if !sone.friend><%if !sone.current><%sone.id> <%/if><%/if><%/foreach>" />
+ <button type="submit"><%= Page.KnownSones.Button.FollowAllSones|l10n|html></button>
+ </form>
+ </div>
+
+ <div>
+ <form action="unfollowSone.html" method="post">
+ <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+ <input type="hidden" name="returnPage" value="<%request.uri|html>" />
+ <input type="hidden" name="sone" value="<%foreach pagination.items sone><%if sone.friend><%sone.id> <%/if><%/foreach>" />
+ <button type="submit"><%= Page.KnownSones.Button.UnfollowAllSones|l10n|html></button>
+ </form>
+ </div>
<div id="known-sones">
<%= page|store key=pageParameter>
--- /dev/null
+<div class="short-text hidden">
+ <%= Notification.Mention.ShortText|l10n|html>
+ <a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
+</div>
+<div class="text">
+ <%= Notification.Mention.Text|l10n|html>
+ <%foreach posts post|unique>
+ <div class="hidden post-id"><%post.id|html></div>
+ <a class="link-<% post.id|html>" href="viewPost.html?post=<% post.id|html>"><% post.sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
+ <%/foreach>
+</div>
+<form class="mark-as-read" action="markAsKnown.html" method="post">
+ <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+ <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+ <input type="hidden" name="type" value="post" />
+ <input type="hidden" name="id" value="<%foreach posts post><% post.id|html><%notlast> <%/notlast><%/foreach>" />
+ <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
+</form>
<div class="short-text hidden">
<%= Notification.NewPost.ShortText|l10n|html>
<a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
</div>
<div class="text">
- <form class="mark-as-read" action="markAsKnown.html" method="post">
- <input type="hidden" name="formPassword" value="<% formPassword|html>" />
- <input type="hidden" name="returnPage" value="<% request.uri|html>" />
- <input type="hidden" name="type" value="post" />
- <input type="hidden" name="id" value="<%foreach posts post><% post.id|html><%notlast> <%/notlast><%/foreach>" />
- <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
- </form>
<%= Notification.NewPost.Text|l10n|html>
<%foreach posts post>
<div class="hidden post-id"><%post.id|html></div>
+<form class="mark-as-read" action="markAsKnown.html" method="post">
+ <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+ <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+ <input type="hidden" name="type" value="reply" />
+ <input type="hidden" name="id" value="<%foreach replies reply><% reply.id|html><%notlast> <%/notlast><%/foreach>" />
+ <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
+</form>
<div class="short-text hidden">
<%= Notification.NewReply.ShortText|l10n|html>
<a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
</div>
<div class="text">
- <form class="mark-as-read" action="markAsKnown.html" method="post">
- <input type="hidden" name="formPassword" value="<% formPassword|html>" />
- <input type="hidden" name="returnPage" value="<% request.uri|html>" />
- <input type="hidden" name="type" value="reply" />
- <input type="hidden" name="id" value="<%foreach replies reply><% reply.id|html><%notlast> <%/notlast><%/foreach>" />
- <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
- </form>
<%foreach replies reply><div class="hidden reply-id"><%reply.id|html></div><%/foreach>
<%= Notification.NewReply.Text|l10n|html>
<%foreach replies postGroup|replyGroup>
+<form class="mark-as-read" action="markAsKnown.html" method="post">
+ <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+ <input type="hidden" name="returnPage" value="<% request.uri|html>" />
+ <input type="hidden" name="type" value="sone" />
+ <input type="hidden" name="id" value="<%foreach sones sone><% sone.id|html><%notlast> <%/notlast><%/foreach>" />
+ <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
+</form>
<div class="short-text hidden">
<%= Notification.NewSone.ShortText|l10n|html>
<a class="link" onclick="showNotificationDetails('<%notification.id|html>'); return false;"><%= Notification.ClickHereToRead|l10n|html></a>
</div>
<div class="text">
- <form class="mark-as-read" action="markAsKnown.html" method="post">
- <input type="hidden" name="formPassword" value="<% formPassword|html>" />
- <input type="hidden" name="returnPage" value="<% request.uri|html>" />
- <input type="hidden" name="type" value="sone" />
- <input type="hidden" name="id" value="<%foreach sones sone><% sone.id|html><%notlast> <%/notlast><%/foreach>" />
- <button type="submit" name="mark-read" value="true"><%= Notification.NewPost.Button.MarkRead|l10n|html></button>
- </form>
<%= Notification.NewSone.Text|l10n|html>
<%foreach sones sone>
- <div class="hidden sone-id"><% sone.id|html></div>
+ <div class="hidden new-sone-id"><% sone.id|html></div>
<a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
<%/foreach>
</div>
+++ /dev/null
-<div class="text">
- <%= Notification.SoneIsBeingRescued.Text|l10n|html>
- <%foreach sones sone>
- <a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
- <%/foreach>
-</div>
--- /dev/null
+<%if soneStatus|match value="inserting">
+ Your Sone <a href="viewSone.html?sone=<%insertSone.id|html>"><%insertSone.niceName|html></a> is now being inserted.
+<%elseif soneStatus|match value="inserted">
+ Your Sone <a href="viewSone.html?sone=<%insertSone.id|html>"><%insertSone.niceName|html></a> has been inserted in <%= Notification.SoneInsert.Duration|l10n 0=insertDuration>.
+<%elseif soneStatus|match value="insert-aborted">
+ Inserting your Sone <a href="viewSone.html?sone=<%insertSone.id|html>"><%insertSone.niceName|html></a> has failed.
+<%/if>
\ No newline at end of file
+++ /dev/null
-<div class="text">
- <%= Notification.SoneRescued.Text|l10n|html>
- <%foreach sones sone>
- <a href="viewSone.html?sone=<% sone.id|html>" title="<% sone.requestUri|html>"><% sone.niceName|html></a><%notlast>,<%/notlast><%last>.<%/last>
- <%/foreach>
- <%= Notification.SoneRescued.Text.RememberToUnlock|l10n|html>
-</div>
getTranslation("WebInterface.DefaultText.Option.PostsPerPage", function(postsPerPageText) {
registerInputTextareaSwap("#sone #options input[name=posts-per-page]", postsPerPageText, "posts-per-page", true, true);
});
+ getTranslation("WebInterface.DefaultText.Option.CharactersPerPost", function(postsPerPageText) {
+ registerInputTextareaSwap("#sone #options input[name=characters-per-post]", postsPerPageText, "characters-per-post", true, true);
+ });
getTranslation("WebInterface.DefaultText.Option.PositiveTrust", function(positiveTrustText) {
registerInputTextareaSwap("#sone #options input[name=positive-trust]", positiveTrustText, "positive-trust", true, true);
});
<%= Page.Options.Option.AutoFollow.Description|l10n|html>
</p>
+ <p>
+ <input type="checkbox" name="enable-sone-insert-notifications"<%ifnull currentSone> disabled="disabled"<%/if><%if enable-sone-insert-notifications> checked="checked"<%/if> />
+ <%= Page.Options.Option.EnableSoneInsertNotifications.Description|l10n|html>
+ </p>
+
<h2><%= Page.Options.Section.RuntimeOptions.Title|l10n|html></h2>
<p><%= Page.Options.Option.InsertionDelay.Description|l10n|html></p>
+ <%if =insertion-delay|in collection=fieldErrors>
+ <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
+ <%/if>
<p><input type="text" name="insertion-delay" value="<% insertion-delay|html>" /></p>
<p><%= Page.Options.Option.PostsPerPage.Description|l10n|html></p>
+ <%if =posts-per-page|in collection=fieldErrors>
+ <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
+ <%/if>
<p><input type="text" name="posts-per-page" value="<% posts-per-page|html>" /></p>
+ <p><%= Page.Options.Option.CharactersPerPost.Description|l10n|html></p>
+ <%if =characters-per-post|in collection=fieldErrors>
+ <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
+ <%/if>
+ <p><input type="text" name="characters-per-post" value="<% characters-per-post|html>" /></p>
+
+ <p>
+ <input type="checkbox" name="require-full-access"<%if require-full-access> checked="checked"<%/if> />
+ <%= Page.Options.Option.RequireFullAccess.Description|l10n|html></p>
+ </p>
+
<h2><%= Page.Options.Section.TrustOptions.Title|l10n|html></h2>
<p><%= Page.Options.Option.PositiveTrust.Description|l10n|html></p>
+ <%if =positive-trust|in collection=fieldErrors>
+ <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
+ <%/if>
<p><input type="text" name="positive-trust" value="<% positive-trust|html>" /></p>
<p><%= Page.Options.Option.NegativeTrust.Description|l10n|html></p>
+ <%if =negative-trust|in collection=fieldErrors>
+ <p class="warning"><%= Page.Options.Warnings.ValueNotChanged|l10n|html></p>
+ <%/if>
<p><input type="text" name="negative-trust" value="<% negative-trust|html>" /></p>
<p><%= Page.Options.Option.TrustComment.Description|l10n|html></p>
<p><input type="text" name="trust-comment" value="<% trust-comment|html>" /></p>
- <h2><%= Page.Options.Section.RescueOptions.Title|l10n|html></h2>
+ <h2><%= Page.Options.Section.FcpOptions.Title|l10n|html></h2>
+
+ <p><input type="checkbox" name="fcp-interface-active"<%if fcp-interface-active> checked="checked"<%/if> /> <%= Page.Options.Option.FcpInterfaceActive.Description|l10n|html></p>
- <p><%= Page.Options.Option.SoneRescueMode.Description|l10n|html></p>
- <p><select name="sone-rescue-mode"><option disabled="disabled"><%= WebInterface.SelectBox.Choose|l10n|html></option><option value="true"<%if sone-rescue-mode> selected="selected"<%/if>><%= WebInterface.SelectBox.Yes|l10n|html></option><option value="false"<%if !sone-rescue-mode> selected="selected"<%/if>><%= WebInterface.SelectBox.No|l10n|html></option></select>
+ <p>
+ <%= Page.Options.Option.FcpFullAccessRequired.Description|l10n|html|replace needle="{link}" replacement='<a href="/config/fcp">'|replace needle="{/link}" replacement='</a>'>
+ <select name="fcp-full-access-required">
+ <option value="0"<%if fcp-full-access-required|match value="0"> selected="selected"<%/if>><%= Page.Options.Option.FcpFullAccessRequired.Value.No|l10n|html></option>
+ <option value="1"<%if fcp-full-access-required|match value="1"> selected="selected"<%/if>><%= Page.Options.Option.FcpFullAccessRequired.Value.Writing|l10n|html></option>
+ <option value="2"<%if fcp-full-access-required|match value="2"> selected="selected"<%/if>><%= Page.Options.Option.FcpFullAccessRequired.Value.Always|l10n|html></option>
+ </select>
+ </p>
<h2><%= Page.Options.Section.Cleaning.Title|l10n|html></h2>
--- /dev/null
+<%include include/head.html>
+
+<h1><%= Page.Rescue.Page.Title|l10n 0=currentSone.niceName|html></h1>
+
+<p><%= Page.Rescue.Text.Description|l10n|html></p>
+<p><%= Page.Rescue.Text.Procedure|l10n|html></p>
+
+<%if soneRescuer.fetching>
+ <p><%= Page.Rescue.Text.Fetching|l10n 0=soneRescuer.currentEdition|html></p>
+<%else>
+ <%if soneRescuer.hasNextEdition>
+ <%if soneRescuer.lastFetchSuccessful>
+ <p><%= Page.Rescue.Text.Fetched|l10n 0=soneRescuer.currentEdition|html></p>
+ <%else>
+ <p><%= Page.Rescue.Text.NotFetched|l10n 0=soneRescuer.currentEdition|html></p>
+ <%/if>
+ <form action="rescue.html" method="post">
+ <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+ <label><%= Page.Rescue.Label.NextEdition|l10n|html></label>
+ <input type="field" name="edition" value="<%soneRescuer.nextEdition>" />
+ <button type="submit" name="fetch" value="true"><%= Page.Rescue.Button.Fetch|l10n|html></button>
+ </form>
+ <%else>
+ <%if soneRescuer.lastFetchSuccessful>
+ <p><%= Page.Rescue.Text.Fetched|l10n 0=soneRescuer.currentEdition|html></p>
+ <%else>
+ <p><%= Page.Rescue.Text.FetchedLast|l10n|html></p>
+ <%/if>
+ <%/if>
+<%/if>
+
+<%include include/tail.html>
<h1><%= Page.ViewSone.Page.TitleWithoutSone|l10n|html></h1>
- <p><%= Page.ViewSone.NoSone.Description|l10n|replace needle="{sone}" replacementKey=sone.id|html></p>
+ <p><%= Page.ViewSone.NoSone.Description|l10n|replace needle="{sone}" replacementKey=soneId|html></p>
<%elseifnull sone.name>
<h1><%= Page.ViewSone.Page.TitleWithoutSone|l10n|html></h1>
<p><%= Page.ViewSone.UnknownSone.Description|l10n|html></p>
+ <p>
+ <%= Page.ViewSone.UnknownSone.LinkToWebOfTrust|l10n|html>
+ <a href="/WebOfTrust/ShowIdentity?id=<% sone.id|html>"><%= Page.ViewSone.Profile.Name.WoTLink|l10n|html></a>
+ </p>
<%else>
<div class="profile-field">
<div class="name"><%= Page.ViewSone.Profile.Label.Name|l10n|html></div>
- <div class="value"><a href="/WebOfTrust/ShowIdentity?id=<% sone.id|html>"><% sone.niceName|html></a></div>
+ <div class="value"><% sone.niceName|html> (<a href="/WebOfTrust/ShowIdentity?id=<% sone.id|html>"><%= Page.ViewSone.Profile.Name.WoTLink|l10n|html></a>)</div>
</div>
<%foreach sone.albums album>
<%foreach posts post>
<%first>
<div id="posts">
- <%include include/pagination.html pagination=postPagination pageParameter==postPage>
+ <%include include/pagination.html pagination=postPagination pageParameter==postPage paginationName==post-navigation>
<%/first>
<%include include/viewPost.html>
<%last>
<%foreach repliedPosts post>
<%first>
- <h2><%= Page.ViewSone.Replies.Title|l10n|html></h2>
+ <h2><%= Page.ViewSone.Replies.Title|l10n|html|replace needle="{sone}" replacementKey=sone.niceName></h2>
<div id="replied-posts">
- <%include include/pagination.html pagination=repliedPostPagination pageParameter==repliedPostPage>
+ <%include include/pagination.html pagination=repliedPostPagination pageParameter==repliedPostPage paginationName==reply-navigation>
<%/first>
<%include include/viewPost.html>
<%last>
+++ /dev/null
-/*
- * Sone - FreenetLinkParserTest.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
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.text;
-
-import java.io.IOException;
-import java.io.StringReader;
-
-import junit.framework.TestCase;
-import net.pterodactylus.util.template.HtmlFilter;
-import net.pterodactylus.util.template.TemplateContextFactory;
-
-/**
- * JUnit test case for {@link FreenetLinkParser}.
- *
- * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
- */
-public class FreenetLinkParserTest extends TestCase {
-
- /**
- * Tests the parser.
- *
- * @throws IOException
- * if an I/O error occurs
- */
- public void testParser() throws IOException {
- TemplateContextFactory templateContextFactory = new TemplateContextFactory();
- templateContextFactory.addFilter("html", new HtmlFilter());
- FreenetLinkParser parser = new FreenetLinkParser(null, templateContextFactory);
- FreenetLinkParserContext context = new FreenetLinkParserContext(null);
- Part part;
-
- part = parser.parse(context, new StringReader("Text."));
- assertEquals("Text.", part.toString());
-
- part = parser.parse(context, new StringReader("Text.\nText."));
- assertEquals("Text.\nText.", part.toString());
-
- part = parser.parse(context, new StringReader("Text.\n\nText."));
- assertEquals("Text.\n\nText.", part.toString());
-
- part = parser.parse(context, new StringReader("Text.\n\n\nText."));
- assertEquals("Text.\n\nText.", part.toString());
-
- part = parser.parse(context, new StringReader("\nText.\n\n\nText."));
- assertEquals("Text.\n\nText.", part.toString());
-
- part = parser.parse(context, new StringReader("\nText.\n\n\nText.\n"));
- assertEquals("Text.\n\nText.", part.toString());
-
- part = parser.parse(context, new StringReader("\nText.\n\n\nText.\n\n"));
- assertEquals("Text.\n\nText.", part.toString());
-
- part = parser.parse(context, new StringReader("\nText.\n\n\n\nText.\n\n\n"));
- assertEquals("Text.\n\nText.", part.toString());
-
- part = parser.parse(context, new StringReader("\n\nText.\n\n\n\nText.\n\n\n"));
- assertEquals("Text.\n\nText.", part.toString());
-
- part = parser.parse(context, new StringReader("\n\nText. KSK@a text.\n\n\n\nText.\n\n\n"));
- assertEquals("Text. <a class=\"freenet\" href=\"/KSK@a\" title=\"KSK@a\">a</a> text.\n\nText.", part.toString());
- }
-
-}
--- /dev/null
+/*
+ * Sone - SoneTextParserTest.java - Copyright © 2011 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 <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.text;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import junit.framework.TestCase;
+import net.pterodactylus.sone.core.SoneProvider;
+import net.pterodactylus.sone.data.Sone;
+
+/**
+ * JUnit test case for {@link SoneTextParser}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneTextParserTest extends TestCase {
+
+ //
+ // ACTIONS
+ //
+
+ /**
+ * Tests basic plain-text operation of the parser.
+ *
+ * @throws IOException
+ * if an I/O error occurs
+ */
+ public void testPlainText() throws IOException {
+ SoneTextParser soneTextParser = new SoneTextParser(null, null);
+ Iterable<Part> parts;
+
+ /* check basic operation. */
+ parts = soneTextParser.parse(null, new StringReader("Test."));
+ assertNotNull("Parts", parts);
+ assertEquals("Part Text", "Test.", convertText(parts, PlainTextPart.class));
+
+ /* check empty lines at start and end. */
+ parts = soneTextParser.parse(null, new StringReader("\nTest.\n\n"));
+ assertNotNull("Parts", parts);
+ assertEquals("Part Text", "Test.", convertText(parts, PlainTextPart.class));
+
+ /* check duplicate empty lines in the text. */
+ parts = soneTextParser.parse(null, new StringReader("\nTest.\n\n\nTest."));
+ assertNotNull("Parts", parts);
+ assertEquals("Part Text", "Test.\n\nTest.", convertText(parts, PlainTextPart.class));
+ }
+
+ /**
+ * Tests parsing of KSK links.
+ *
+ * @throws IOException
+ * if an I/O error occurs
+ */
+ public void testKSKLinks() throws IOException {
+ SoneTextParser soneTextParser = new SoneTextParser(null, null);
+ Iterable<Part> parts;
+
+ /* check basic links. */
+ parts = soneTextParser.parse(null, new StringReader("KSK@gpl.txt"));
+ assertNotNull("Parts", parts);
+ assertEquals("Part Text", "[KSK@gpl.txt|gpl.txt|gpl.txt]", convertText(parts, FreenetLinkPart.class));
+
+ /* check embedded links. */
+ parts = soneTextParser.parse(null, new StringReader("Link is KSK@gpl.txt\u200b."));
+ assertNotNull("Parts", parts);
+ assertEquals("Part Text", "Link is [KSK@gpl.txt|gpl.txt|gpl.txt]\u200b.", convertText(parts, PlainTextPart.class, FreenetLinkPart.class));
+
+ /* check embedded links and line breaks. */
+ parts = soneTextParser.parse(null, new StringReader("Link is KSK@gpl.txt\nKSK@test.dat\n"));
+ assertNotNull("Parts", parts);
+ assertEquals("Part Text", "Link is [KSK@gpl.txt|gpl.txt|gpl.txt]\n[KSK@test.dat|test.dat|test.dat]", convertText(parts, PlainTextPart.class, FreenetLinkPart.class));
+ }
+
+ /**
+ * Test case for a bug that was discovered in 0.6.7.
+ *
+ * @throws IOException
+ * if an I/O error occurs
+ */
+ @SuppressWarnings("synthetic-access")
+ public void testEmptyLinesAndSoneLinks() throws IOException {
+ SoneTextParser soneTextParser = new SoneTextParser(new TestSoneProvider(), null);
+ Iterable<Part> parts;
+
+ /* check basic links. */
+ parts = soneTextParser.parse(null, new StringReader("Some text.\n\nLink to sone://DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU and stuff."));
+ assertNotNull("Parts", parts);
+ assertEquals("Part Text", "Some text.\n\nLink to [Sone|DAxKQzS48mtaQc7sUVHIgx3fnWZPQBz0EueBreUVWrU] and stuff.", convertText(parts, PlainTextPart.class, SonePart.class));
+ }
+
+ //
+ // PRIVATE METHODS
+ //
+
+ /**
+ * Converts all given {@link Part}s into a string, validating that the
+ * part’s classes match only the expected classes.
+ *
+ * @param parts
+ * The parts to convert to text
+ * @param validClasses
+ * The valid classes; if no classes are given, all classes are
+ * valid
+ * @return The converted text
+ */
+ private String convertText(Iterable<Part> parts, Class<?>... validClasses) {
+ StringBuilder text = new StringBuilder();
+ for (Part part : parts) {
+ assertNotNull("Part", part);
+ boolean classValid = validClasses.length == 0;
+ for (Class<?> validClass : validClasses) {
+ if (validClass.isAssignableFrom(part.getClass())) {
+ classValid = true;
+ break;
+ }
+ }
+ if (!classValid) {
+ assertEquals("Part’s Class", null, part.getClass());
+ }
+ if (part instanceof PlainTextPart) {
+ text.append(((PlainTextPart) part).getText());
+ } else if (part instanceof FreenetLinkPart) {
+ FreenetLinkPart freenetLinkPart = (FreenetLinkPart) part;
+ text.append('[').append(freenetLinkPart.getLink()).append('|').append(freenetLinkPart.isTrusted() ? "trusted|" : "").append(freenetLinkPart.getTitle()).append('|').append(freenetLinkPart.getText()).append(']');
+ } else if (part instanceof LinkPart) {
+ LinkPart linkPart = (LinkPart) part;
+ text.append('[').append(linkPart.getLink()).append('|').append(linkPart.getTitle()).append('|').append(linkPart.getText()).append(']');
+ } else if (part instanceof SonePart) {
+ SonePart sonePart = (SonePart) part;
+ text.append("[Sone|").append(sonePart.getSone().getId()).append(']');
+ }
+ }
+ return text.toString();
+ }
+
+ /**
+ * Mock Sone provider.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+ private static class TestSoneProvider implements SoneProvider {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Sone getSone(final String soneId, boolean create) {
+ return new Sone(soneId) {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getName() {
+ return soneId;
+ }
+ };
+ }
+
+ }
+
+}