Merge branch 'release-0.9.6'
[Sone.git] / src / main / java / net / pterodactylus / sone / fcp / AbstractSoneCommand.java
1 /*
2  * Sone - AbstractSoneCommand.java - Copyright © 2011–2016 David Roden
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 package net.pterodactylus.sone.fcp;
19
20 import java.util.Collection;
21 import java.util.List;
22
23 import net.pterodactylus.sone.core.Core;
24 import net.pterodactylus.sone.data.Post;
25 import net.pterodactylus.sone.data.PostReply;
26 import net.pterodactylus.sone.data.Profile;
27 import net.pterodactylus.sone.data.Profile.Field;
28 import net.pterodactylus.sone.data.Reply;
29 import net.pterodactylus.sone.data.Sone;
30 import net.pterodactylus.sone.freenet.SimpleFieldSetBuilder;
31 import net.pterodactylus.sone.freenet.fcp.AbstractCommand;
32 import net.pterodactylus.sone.freenet.fcp.Command;
33 import net.pterodactylus.sone.freenet.fcp.FcpException;
34 import net.pterodactylus.sone.template.SoneAccessor;
35
36 import com.google.common.base.Optional;
37 import com.google.common.collect.Collections2;
38
39 import freenet.node.FSParseException;
40 import freenet.support.SimpleFieldSet;
41
42 /**
43  * Abstract base implementation of a {@link Command} with Sone-related helper
44  * methods.
45  *
46  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
47  */
48 public abstract class AbstractSoneCommand extends AbstractCommand {
49
50         /** The Sone core. */
51         private final Core core;
52
53         /** Whether this command needs write access. */
54         private final boolean writeAccess;
55
56         /**
57          * Creates a new abstract Sone FCP command.
58          *
59          * @param core
60          *            The Sone core
61          */
62         protected AbstractSoneCommand(Core core) {
63                 this(core, false);
64         }
65
66         /**
67          * Creates a new abstract Sone FCP command.
68          *
69          * @param core
70          *            The Sone core
71          * @param writeAccess
72          *            {@code true} if this command requires write access,
73          *            {@code false} otherwise
74          */
75         protected AbstractSoneCommand(Core core, boolean writeAccess) {
76                 this.core = core;
77                 this.writeAccess = writeAccess;
78         }
79
80         //
81         // ACCESSORS
82         //
83
84         /**
85          * Returns the Sone core.
86          *
87          * @return The Sone core
88          */
89         protected Core getCore() {
90                 return core;
91         }
92
93         /**
94          * Returns whether this command requires write access.
95          *
96          * @return {@code true} if this command require write access, {@code false}
97          *         otherwise
98          */
99         public boolean requiresWriteAccess() {
100                 return writeAccess;
101         }
102
103         //
104         // PROTECTED METHODS
105         //
106
107         /**
108          * Encodes text in a way that makes it possible for the text to be stored in
109          * a {@link SimpleFieldSet}. Backslashes, CR, and LF are prepended with a
110          * backslash.
111          *
112          * @param text
113          *            The text to encode
114          * @return The encoded text
115          */
116         protected static String encodeString(String text) {
117                 return text.replaceAll("\\\\", "\\\\").replaceAll("\n", "\\\\n").replaceAll("\r", "\\\\r");
118         }
119
120         /**
121          * Returns a Sone whose ID is a parameter in the given simple field set.
122          *
123          * @param simpleFieldSet
124          *            The simple field set containing the ID of the Sone
125          * @param parameterName
126          *            The name under which the Sone ID is stored in the simple field
127          *            set
128          * @param localOnly
129          *            {@code true} to only return local Sones, {@code false} to
130          *            return any Sones
131          * @return The Sone
132          * @throws FcpException
133          *             if there is no Sone ID stored under the given parameter name,
134          *             or if the Sone ID is invalid
135          */
136         protected Sone getSone(SimpleFieldSet simpleFieldSet, String parameterName, boolean localOnly) throws FcpException {
137                 return getSone(simpleFieldSet, parameterName, localOnly, true).get();
138         }
139
140         /**
141          * Returns a Sone whose ID is a parameter in the given simple field set.
142          *
143          * @param simpleFieldSet
144          *            The simple field set containing the ID of the Sone
145          * @param parameterName
146          *            The name under which the Sone ID is stored in the simple field
147          *            set
148          * @param localOnly
149          *            {@code true} to only return local Sones, {@code false} to
150          *            return any Sones
151          * @param mandatory
152          *            {@code true} if a valid Sone ID is required, {@code false}
153          *            otherwise
154          * @return The Sone, or {@code null} if {@code mandatory} is {@code false}
155          *         and the Sone ID is invalid
156          * @throws FcpException
157          *             if there is no Sone ID stored under the given parameter name,
158          *             or if {@code mandatory} is {@code true} and the Sone ID is
159          *             invalid
160          */
161         protected Optional<Sone> getSone(SimpleFieldSet simpleFieldSet, String parameterName, boolean localOnly, boolean mandatory) throws FcpException {
162                 String soneId = simpleFieldSet.get(parameterName);
163                 if (mandatory && (soneId == null)) {
164                         throw new FcpException("Could not load Sone ID from “" + parameterName + "”.");
165                 }
166                 Optional<Sone> sone = core.getSone(soneId);
167                 if ((mandatory && !sone.isPresent()) || (mandatory && sone.isPresent() && (localOnly && !sone.get().isLocal()))) {
168                         throw new FcpException("Could not load Sone from “" + soneId + "”.");
169                 }
170                 return sone;
171         }
172
173         /**
174          * Returns a post whose ID is a parameter in the given simple field set.
175          *
176          * @param simpleFieldSet
177          *            The simple field set containing the ID of the post
178          * @param parameterName
179          *            The name under which the post ID is stored in the simple field
180          *            set
181          * @return The post
182          * @throws FcpException
183          *             if there is no post ID stored under the given parameter name,
184          *             or if the post ID is invalid
185          */
186         protected Post getPost(SimpleFieldSet simpleFieldSet, String parameterName) throws FcpException {
187                 try {
188                         String postId = simpleFieldSet.getString(parameterName);
189                         Optional<Post> post = core.getPost(postId);
190                         if (!post.isPresent()) {
191                                 throw new FcpException("Could not load post from “" + postId + "”.");
192                         }
193                         return post.get();
194                 } catch (FSParseException fspe1) {
195                         throw new FcpException("Could not post ID from “" + parameterName + "”.", fspe1);
196                 }
197         }
198
199         /**
200          * Returns a reply whose ID is a parameter in the given simple field set.
201          *
202          * @param simpleFieldSet
203          *            The simple field set containing the ID of the reply
204          * @param parameterName
205          *            The name under which the reply ID is stored in the simple
206          *            field set
207          * @return The reply
208          * @throws FcpException
209          *             if there is no reply ID stored under the given parameter
210          *             name, or if the reply ID is invalid
211          */
212         protected PostReply getReply(SimpleFieldSet simpleFieldSet, String parameterName) throws FcpException {
213                 try {
214                         String replyId = simpleFieldSet.getString(parameterName);
215                         Optional<PostReply> reply = core.getPostReply(replyId);
216                         if (!reply.isPresent()) {
217                                 throw new FcpException("Could not load reply from “" + replyId + "”.");
218                         }
219                         return reply.get();
220                 } catch (FSParseException fspe1) {
221                         throw new FcpException("Could not reply ID from “" + parameterName + "”.", fspe1);
222                 }
223         }
224
225         /**
226          * Creates a simple field set from the given Sone, including {@link Profile}
227          * information.
228          *
229          * @param sone
230          *            The Sone to encode
231          * @param prefix
232          *            The prefix for the field names (may be empty but not {@code
233          *            null})
234          * @param localSone
235          *            An optional local Sone that is used for Sone-specific data,
236          *            such as if the Sone is followed by the local Sone
237          * @return The simple field set containing the given Sone
238          */
239         protected static SimpleFieldSet encodeSone(Sone sone, String prefix, Optional<Sone> localSone) {
240                 SimpleFieldSetBuilder soneBuilder = new SimpleFieldSetBuilder();
241
242                 soneBuilder.put(prefix + "Name", sone.getName());
243                 soneBuilder.put(prefix + "NiceName", SoneAccessor.getNiceName(sone));
244                 soneBuilder.put(prefix + "LastUpdated", sone.getTime());
245                 if (localSone.isPresent()) {
246                         soneBuilder.put(prefix + "Followed", String.valueOf(localSone.get().hasFriend(sone.getId())));
247                 }
248                 Profile profile = sone.getProfile();
249                 soneBuilder.put(prefix + "Field.Count", profile.getFields().size());
250                 int fieldIndex = 0;
251                 for (Field field : profile.getFields()) {
252                         soneBuilder.put(prefix + "Field." + fieldIndex + ".Name", field.getName());
253                         soneBuilder.put(prefix + "Field." + fieldIndex + ".Value", field.getValue());
254                         ++fieldIndex;
255                 }
256
257                 return soneBuilder.get();
258         }
259
260         /**
261          * Creates a simple field set from the given collection of Sones.
262          *
263          * @param sones
264          *            The Sones to encode
265          * @param prefix
266          *            The prefix for the field names (may be empty but not
267          *            {@code null})
268          * @return The simple field set containing the given Sones
269          */
270         protected static SimpleFieldSet encodeSones(Collection<? extends Sone> sones, String prefix) {
271                 SimpleFieldSetBuilder soneBuilder = new SimpleFieldSetBuilder();
272
273                 int soneIndex = 0;
274                 soneBuilder.put(prefix + "Count", sones.size());
275                 for (Sone sone : sones) {
276                         String sonePrefix = prefix + soneIndex++ + ".";
277                         soneBuilder.put(sonePrefix + "ID", sone.getId());
278                         soneBuilder.put(sonePrefix + "Name", sone.getName());
279                         soneBuilder.put(sonePrefix + "NiceName", SoneAccessor.getNiceName(sone));
280                         soneBuilder.put(sonePrefix + "Time", sone.getTime());
281                 }
282
283                 return soneBuilder.get();
284         }
285
286         /**
287          * Creates a simple field set from the given post.
288          *
289          * @param post
290          *            The post to encode
291          * @param prefix
292          *            The prefix for the field names (may be empty but not
293          *            {@code null})
294          * @param includeReplies
295          *            {@code true} to include replies, {@code false} to not include
296          *            replies
297          * @return The simple field set containing the post
298          */
299         protected SimpleFieldSet encodePost(Post post, String prefix, boolean includeReplies) {
300                 SimpleFieldSetBuilder postBuilder = new SimpleFieldSetBuilder();
301
302                 postBuilder.put(prefix + "ID", post.getId());
303                 postBuilder.put(prefix + "Sone", post.getSone().getId());
304                 if (post.getRecipientId().isPresent()) {
305                         postBuilder.put(prefix + "Recipient", post.getRecipientId().get());
306                 }
307                 postBuilder.put(prefix + "Time", post.getTime());
308                 postBuilder.put(prefix + "Text", encodeString(post.getText()));
309                 postBuilder.put(encodeLikes(core.getLikes(post), prefix + "Likes."));
310
311                 if (includeReplies) {
312                         List<PostReply> replies = core.getReplies(post.getId());
313                         postBuilder.put(encodeReplies(replies, prefix));
314                 }
315
316                 return postBuilder.get();
317         }
318
319         /**
320          * Creates a simple field set from the given collection of posts.
321          *
322          * @param posts
323          *            The posts to encode
324          * @param prefix
325          *            The prefix for the field names (may be empty but not
326          *            {@code null})
327          * @param includeReplies
328          *            {@code true} to include the replies, {@code false} to not
329          *            include the replies
330          * @return The simple field set containing the posts
331          */
332         protected SimpleFieldSet encodePosts(Collection<? extends Post> posts, String prefix, boolean includeReplies) {
333                 SimpleFieldSetBuilder postBuilder = new SimpleFieldSetBuilder();
334
335                 int postIndex = 0;
336                 postBuilder.put(prefix + "Count", posts.size());
337                 for (Post post : posts) {
338                         String postPrefix = prefix + postIndex++;
339                         postBuilder.put(encodePost(post, postPrefix + ".", includeReplies));
340                         if (includeReplies) {
341                                 postBuilder.put(encodeReplies(Collections2.filter(core.getReplies(post.getId()), Reply.FUTURE_REPLY_FILTER), postPrefix + "."));
342                         }
343                 }
344
345                 return postBuilder.get();
346         }
347
348         /**
349          * Creates a simple field set from the given collection of replies.
350          *
351          * @param replies
352          *            The replies to encode
353          * @param prefix
354          *            The prefix for the field names (may be empty, but not
355          *            {@code null})
356          * @return The simple field set containing the replies
357          */
358         protected static SimpleFieldSet encodeReplies(Collection<? extends PostReply> replies, String prefix) {
359                 SimpleFieldSetBuilder replyBuilder = new SimpleFieldSetBuilder();
360
361                 int replyIndex = 0;
362                 replyBuilder.put(prefix + "Replies.Count", replies.size());
363                 for (PostReply reply : replies) {
364                         String replyPrefix = prefix + "Replies." + replyIndex++ + ".";
365                         replyBuilder.put(replyPrefix + "ID", reply.getId());
366                         replyBuilder.put(replyPrefix + "Sone", reply.getSone().getId());
367                         replyBuilder.put(replyPrefix + "Time", reply.getTime());
368                         replyBuilder.put(replyPrefix + "Text", encodeString(reply.getText()));
369                 }
370
371                 return replyBuilder.get();
372         }
373
374         /**
375          * Creates a simple field set from the given collection of Sones that like
376          * an element.
377          *
378          * @param likes
379          *            The liking Sones
380          * @param prefix
381          *            The prefix for the field names (may be empty but not
382          *            {@code null})
383          * @return The simple field set containing the likes
384          */
385         protected static SimpleFieldSet encodeLikes(Collection<? extends Sone> likes, String prefix) {
386                 SimpleFieldSetBuilder likesBuilder = new SimpleFieldSetBuilder();
387
388                 int likeIndex = 0;
389                 likesBuilder.put(prefix + "Count", likes.size());
390                 for (Sone sone : likes) {
391                         String sonePrefix = prefix + likeIndex++ + ".";
392                         likesBuilder.put(sonePrefix + "ID", sone.getId());
393                 }
394
395                 return likesBuilder.get();
396         }
397
398         //
399         // OBJECT METHODS
400         //
401
402         /**
403          * {@inheritDoc}
404          */
405         @Override
406         public String toString() {
407                 return getClass().getName() + "[writeAccess=" + writeAccess + "]";
408         }
409
410 }