X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=blobdiff_plain;f=src%2Fmain%2Fjava%2Fnet%2Fpterodactylus%2Fsone%2Fweb%2FSearchPage.java;h=8b444a7cbd3ff5e52478403fcf6f57ff7d1b3ff9;hp=6f5e001ad290d6451f382c99fb83ee2c1ac76ddc;hb=7b55e0be6a3283e43a9bbab98f82aebdd948eb33;hpb=eb776984f2bf7c1f218dcc65fca80ef0298ed2f2 diff --git a/src/main/java/net/pterodactylus/sone/web/SearchPage.java b/src/main/java/net/pterodactylus/sone/web/SearchPage.java index 6f5e001..8b444a7 100644 --- a/src/main/java/net/pterodactylus/sone/web/SearchPage.java +++ b/src/main/java/net/pterodactylus/sone/web/SearchPage.java @@ -1,5 +1,5 @@ /* - * Sone - SearchPage.java - Copyright © 2010 David Roden + * Sone - SearchPage.java - Copyright © 2010–2016 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 @@ -17,6 +17,10 @@ package net.pterodactylus.sone.web; +import static com.google.common.base.Optional.fromNullable; +import static com.google.common.primitives.Ints.tryParse; +import static java.util.logging.Logger.getLogger; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -24,26 +28,33 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import net.pterodactylus.sone.data.Post; +import net.pterodactylus.sone.data.PostReply; import net.pterodactylus.sone.data.Profile; import net.pterodactylus.sone.data.Profile.Field; import net.pterodactylus.sone.data.Reply; import net.pterodactylus.sone.data.Sone; -import net.pterodactylus.util.collection.Mapper; -import net.pterodactylus.util.collection.Mappers; +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.logging.Logging; -import net.pterodactylus.util.number.Numbers; import net.pterodactylus.util.template.Template; import net.pterodactylus.util.template.TemplateContext; import net.pterodactylus.util.text.StringEscaper; import net.pterodactylus.util.text.TextException; +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.base.Predicate; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.Collections2; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.Ordering; + /** * This page lets the user search for posts and replies that contain certain * words. @@ -53,7 +64,21 @@ import net.pterodactylus.util.text.TextException; public class SearchPage extends SoneTemplatePage { /** The logger. */ - private static final Logger logger = Logging.getLogger(SearchPage.class); + private static final Logger logger = getLogger(SearchPage.class.getName()); + + /** Short-term cache. */ + private final LoadingCache, Set>> hitCache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build(new CacheLoader, Set>>() { + + @Override + @SuppressWarnings("synthetic-access") + public Set> load(List phrases) { + Set posts = new HashSet(); + for (Sone sone : webInterface.getCore().getSones()) { + posts.addAll(sone.getPosts()); + } + return getHits(Collections2.filter(posts, Post.FUTURE_POSTS_FILTER), phrases, new PostStringGenerator()); + } + }); /** * Creates a new search page. @@ -75,42 +100,58 @@ public class SearchPage extends SoneTemplatePage { * {@inheritDoc} */ @Override - protected void processTemplate(Request request, TemplateContext templateContext) throws RedirectException { - super.processTemplate(request, templateContext); + @SuppressWarnings("synthetic-access") + protected void handleRequest(FreenetRequest request, TemplateContext templateContext) throws RedirectException { String query = request.getHttpRequest().getParam("query").trim(); if (query.length() == 0) { throw new RedirectException("index.html"); } List phrases = parseSearchPhrases(query); + if (phrases.isEmpty()) { + throw new RedirectException("index.html"); + } + + /* check for a couple of shortcuts. */ + if (phrases.size() == 1) { + String phrase = phrases.get(0).getPhrase(); + + /* is it a Sone ID? */ + redirectIfNotNull(getSoneId(phrase), "viewSone.html?sone="); + + /* is it a post ID? */ + redirectIfNotNull(getPostId(phrase), "viewPost.html?post="); - Set sones = webInterface.getCore().getSones(); - Set> soneHits = getHits(sones, phrases, SoneStringGenerator.COMPLETE_GENERATOR); + /* is it a reply ID? show the post. */ + redirectIfNotNull(getReplyPostId(phrase), "viewPost.html?post="); - Set posts = new HashSet(); - for (Sone sone : sones) { - posts.addAll(sone.getPosts()); + /* is it an album ID? */ + redirectIfNotNull(getAlbumId(phrase), "imageBrowser.html?album="); + + /* is it an image ID? */ + redirectIfNotNull(getImageId(phrase), "imageBrowser.html?image="); } - @SuppressWarnings("synthetic-access") - Set> postHits = getHits(Filters.filteredSet(posts, Post.FUTURE_POSTS_FILTER), phrases, new PostStringGenerator()); + + Collection sones = webInterface.getCore().getSones(); + Collection> soneHits = getHits(sones, phrases, SoneStringGenerator.COMPLETE_GENERATOR); + + Collection> postHits = hitCache.getUnchecked(phrases); /* now filter. */ - soneHits = Filters.filteredSet(soneHits, Hit.POSITIVE_FILTER); - postHits = Filters.filteredSet(postHits, Hit.POSITIVE_FILTER); + soneHits = Collections2.filter(soneHits, Hit.POSITIVE_FILTER); + postHits = Collections2.filter(postHits, Hit.POSITIVE_FILTER); /* now sort. */ - List> sortedSoneHits = new ArrayList>(soneHits); - Collections.sort(sortedSoneHits, Hit.DESCENDING_COMPARATOR); - List> sortedPostHits = new ArrayList>(postHits); - Collections.sort(sortedPostHits, Hit.DESCENDING_COMPARATOR); + List> sortedSoneHits = Ordering.from(Hit.DESCENDING_COMPARATOR).sortedCopy(soneHits); + List> sortedPostHits = Ordering.from(Hit.DESCENDING_COMPARATOR).sortedCopy(postHits); /* extract Sones and posts. */ - List resultSones = Mappers.mappedList(sortedSoneHits, new HitMapper()); - List resultPosts = Mappers.mappedList(sortedPostHits, new HitMapper()); + List resultSones = FluentIterable.from(sortedSoneHits).transform(new HitMapper()).toList(); + List resultPosts = FluentIterable.from(sortedPostHits).transform(new HitMapper()).toList(); /* pagination. */ - Pagination sonePagination = new Pagination(resultSones, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("sonePage"), 0)); - Pagination postPagination = new Pagination(resultPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("postPage"), 0)); + Pagination sonePagination = new Pagination(resultSones, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(fromNullable(tryParse(request.getHttpRequest().getParam("sonePage"))).or(0)); + Pagination postPagination = new Pagination(resultPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(fromNullable(tryParse(request.getHttpRequest().getParam("postPage"))).or(0)); templateContext.set("sonePagination", sonePagination); templateContext.set("soneHits", sonePagination.getItems()); @@ -139,7 +180,7 @@ public class SearchPage extends SoneTemplatePage { * The string generator for the objects * @return The hits for the given phrases */ - private Set> getHits(Collection objects, List phrases, StringGenerator stringGenerator) { + private static Set> getHits(Collection objects, List phrases, StringGenerator stringGenerator) { Set> hits = new HashSet>(); for (T object : objects) { String objectString = stringGenerator.generateString(object); @@ -160,8 +201,8 @@ public class SearchPage extends SoneTemplatePage { * The query to parse * @return The parsed phrases */ - private List parseSearchPhrases(String query) { - List parsedPhrases = null; + private static List parseSearchPhrases(String query) { + List parsedPhrases; try { parsedPhrases = StringEscaper.parseLine(query); } catch (TextException te1) { @@ -172,11 +213,20 @@ public class SearchPage extends SoneTemplatePage { List phrases = new ArrayList(); 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; } @@ -191,8 +241,8 @@ public class SearchPage extends SoneTemplatePage { * The expression to search * @return The score of the expression */ - private double calculateScore(List phrases, String expression) { - logger.log(Level.FINEST, "Calculating Score for “%s”…", expression); + private static double calculateScore(List phrases, String expression) { + logger.log(Level.FINEST, String.format("Calculating Score for “%s”…", expression)); double optionalHits = 0; double requiredHits = 0; int forbiddenHits = 0; @@ -212,10 +262,10 @@ public class SearchPage extends SoneTemplatePage { } score += Math.pow(1 - position / (double) expression.length(), 2); index = position + phraseString.length(); - logger.log(Level.FINEST, "Got hit at position %d.", position); + logger.log(Level.FINEST, String.format("Got hit at position %d.", position)); ++matches; } - logger.log(Level.FINEST, "Score: %f", score); + logger.log(Level.FINEST, String.format("Score: %f", score)); if (matches == 0) { continue; } @@ -233,6 +283,95 @@ public class SearchPage extends SoneTemplatePage { } /** + * Throws a + * {@link net.pterodactylus.sone.web.page.FreenetTemplatePage.RedirectException} + * if the given object is not {@code null}, appending the object to the + * given target URL. + * + * @param object + * The object on which to redirect + * @param target + * The target of the redirect + * @throws RedirectException + * if {@code object} is not {@code null} + */ + private static void redirectIfNotNull(String object, String target) throws RedirectException { + if (object != null) { + throw new RedirectException(target + object); + } + } + + /** + * If the given phrase contains a Sone ID (optionally prefixed by + * “sone://”), returns said Sone ID, otherwise return {@code null}. + * + * @param phrase + * The phrase that maybe is a Sone ID + * @return The Sone ID, or {@code null} + */ + private String getSoneId(String phrase) { + String soneId = phrase.startsWith("sone://") ? phrase.substring(7) : phrase; + return (webInterface.getCore().getSone(soneId).isPresent()) ? soneId : null; + } + + /** + * If the given phrase contains a post ID (optionally prefixed by + * “post://”), returns said post ID, otherwise return {@code null}. + * + * @param phrase + * The phrase that maybe is a post ID + * @return The post ID, or {@code null} + */ + private String getPostId(String phrase) { + String postId = phrase.startsWith("post://") ? phrase.substring(7) : phrase; + return (webInterface.getCore().getPost(postId).isPresent()) ? postId : null; + } + + /** + * If the given phrase contains a reply ID (optionally prefixed by + * “reply://”), returns the ID of the post the reply belongs to, otherwise + * return {@code null}. + * + * @param phrase + * The phrase that maybe is a reply ID + * @return The reply’s post ID, or {@code null} + */ + private String getReplyPostId(String phrase) { + String replyId = phrase.startsWith("reply://") ? phrase.substring(8) : phrase; + Optional postReply = webInterface.getCore().getPostReply(replyId); + if (!postReply.isPresent()) { + return null; + } + return postReply.get().getPostId(); + } + + /** + * If the given phrase contains an album ID (optionally prefixed by + * “album://”), returns said album ID, otherwise return {@code null}. + * + * @param phrase + * The phrase that maybe is an album ID + * @return The album ID, or {@code null} + */ + private String getAlbumId(String phrase) { + String albumId = phrase.startsWith("album://") ? phrase.substring(8) : phrase; + return (webInterface.getCore().getAlbum(albumId) != null) ? albumId : null; + } + + /** + * If the given phrase contains an image ID (optionally prefixed by + * “image://”), returns said image ID, otherwise return {@code null}. + * + * @param phrase + * The phrase that maybe is an image ID + * @return The image ID, or {@code null} + */ + private String getImageId(String phrase) { + String imageId = phrase.startsWith("image://") ? phrase.substring(8) : phrase; + return (webInterface.getCore().getImage(imageId, false) != null) ? imageId : null; + } + + /** * Converts a given object into a {@link String}. * * @param @@ -326,10 +465,10 @@ public class SearchPage extends SoneTemplatePage { public String generateString(Post post) { StringBuilder postString = new StringBuilder(); postString.append(post.getText()); - if (post.getRecipient() != null) { - postString.append(' ').append(SoneStringGenerator.NAME_GENERATOR.generateString(post.getRecipient())); + if (post.getRecipient().isPresent()) { + postString.append(' ').append(SoneStringGenerator.NAME_GENERATOR.generateString(post.getRecipient().get())); } - for (Reply reply : Filters.filteredList(webInterface.getCore().getReplies(post), Reply.FUTURE_REPLIES_FILTER)) { + for (PostReply reply : Collections2.filter(webInterface.getCore().getReplies(post.getId()), Reply.FUTURE_REPLY_FILTER)) { postString.append(' ').append(SoneStringGenerator.NAME_GENERATOR.generateString(reply.getSone())); postString.append(' ').append(reply.getText()); } @@ -401,6 +540,30 @@ public class SearchPage extends SoneTemplatePage { 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; + } + Phrase phrase = (Phrase) object; + return (this.optionality == phrase.optionality) && this.phrase.equals(phrase.phrase); + } + } /** @@ -415,11 +578,11 @@ public class SearchPage extends SoneTemplatePage { private static class Hit { /** Filter for {@link Hit}s with a score of more than 0. */ - public static final Filter> POSITIVE_FILTER = new Filter>() { + public static final Predicate> POSITIVE_FILTER = new Predicate>() { @Override - public boolean filterObject(Hit hit) { - return hit.getScore() > 0; + public boolean apply(Hit hit) { + return (hit != null) && (hit.getScore() > 0); } }; @@ -429,7 +592,7 @@ public class SearchPage extends SoneTemplatePage { @Override public int compare(Hit leftHit, Hit rightHit) { - return (rightHit.getScore() < leftHit.getScore()) ? -1 : ((rightHit.getScore() > leftHit.getScore()) ? 1 : 0); + return Double.compare(rightHit.getScore(), leftHit.getScore()); } }; @@ -480,13 +643,13 @@ public class SearchPage extends SoneTemplatePage { * The type of the object to extract * @author David ‘Bombe’ Roden */ - public static class HitMapper implements Mapper, T> { + private static class HitMapper implements Function, T> { /** * {@inheritDoc} */ @Override - public T map(Hit input) { + public T apply(Hit input) { return input.getObject(); }