X-Git-Url: https://git.pterodactylus.net/?p=Sone.git;a=blobdiff_plain;f=src%2Fmain%2Fkotlin%2Fnet%2Fpterodactylus%2Fsone%2Fweb%2Fpages%2FSearchPage.kt;h=9bbc3e5baa5e6605b35c49233a8243a0b3c955e7;hp=aff6aaf8bc0ed09c0686e8c7afd00ae755dd4f47;hb=0ace0221fc20ef93457b8c62c6ac12286c1aa12c;hpb=3cb0e2c78bb37f2d70ecde92ad4fcf642de788a3 diff --git a/src/main/kotlin/net/pterodactylus/sone/web/pages/SearchPage.kt b/src/main/kotlin/net/pterodactylus/sone/web/pages/SearchPage.kt index aff6aaf..9bbc3e5 100644 --- a/src/main/kotlin/net/pterodactylus/sone/web/pages/SearchPage.kt +++ b/src/main/kotlin/net/pterodactylus/sone/web/pages/SearchPage.kt @@ -1,38 +1,38 @@ package net.pterodactylus.sone.web.pages import com.google.common.base.Ticker -import com.google.common.cache.Cache -import com.google.common.cache.CacheBuilder -import net.pterodactylus.sone.data.Post -import net.pterodactylus.sone.data.PostReply -import net.pterodactylus.sone.data.Sone -import net.pterodactylus.sone.utils.Pagination -import net.pterodactylus.sone.utils.emptyToNull -import net.pterodactylus.sone.utils.paginate -import net.pterodactylus.sone.utils.parameters -import net.pterodactylus.sone.web.WebInterface -import net.pterodactylus.sone.web.page.FreenetRequest -import net.pterodactylus.sone.web.pages.SearchPage.Optionality.FORBIDDEN -import net.pterodactylus.sone.web.pages.SearchPage.Optionality.OPTIONAL -import net.pterodactylus.sone.web.pages.SearchPage.Optionality.REQUIRED -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 java.util.concurrent.TimeUnit.MINUTES +import com.google.common.cache.* +import freenet.support.* +import net.pterodactylus.sone.data.* +import net.pterodactylus.sone.main.* +import net.pterodactylus.sone.utils.* +import net.pterodactylus.sone.web.* +import net.pterodactylus.sone.web.page.* +import net.pterodactylus.sone.web.pages.SearchPage.Optionality.* +import net.pterodactylus.util.template.* +import net.pterodactylus.util.text.* +import java.util.concurrent.TimeUnit.* +import javax.inject.* /** * This page lets the user search for posts and replies that contain certain * words. */ -class SearchPage @JvmOverloads constructor(template: Template, webInterface: WebInterface, ticker: Ticker = Ticker.systemTicker()): - SoneTemplatePage("search.html", template, "Page.Search.Title", webInterface, false) { +@TemplatePath("/templates/search.html") +@ToadletPath("search.html") +class SearchPage(webInterface: WebInterface, loaders: Loaders, templateRenderer: TemplateRenderer, ticker: Ticker = Ticker.systemTicker()) : + SoneTemplatePage(webInterface, loaders, templateRenderer, pageTitleKey = "Page.Search.Title") { + + @Inject + constructor(webInterface: WebInterface, loaders: Loaders, templateRenderer: TemplateRenderer) : + this(webInterface, loaders, templateRenderer, Ticker.systemTicker()) private val cache: Cache, Pagination> = CacheBuilder.newBuilder().ticker(ticker).expireAfterAccess(5, MINUTES).build() - override fun handleRequest(freenetRequest: FreenetRequest, templateContext: TemplateContext) { + override fun handleRequest(soneRequest: SoneRequest, templateContext: TemplateContext) { + val startTime = System.currentTimeMillis() val phrases = try { - freenetRequest.parameters["query"].emptyToNull?.parse() + soneRequest.parameters["query"].emptyToNull?.parse() } catch (te: TextException) { redirect("index.html") } @@ -42,83 +42,80 @@ class SearchPage @JvmOverloads constructor(template: Template, webInterface: Web 0 -> redirect("index.html") 1 -> phrases.first().phrase.also { word -> when { - word.removePrefix("sone://").let(webInterface.core::getSone).isPresent -> redirect("viewSone.html?sone=${word.removePrefix("sone://")}") - word.removePrefix("post://").let(webInterface.core::getPost).isPresent -> redirect("viewPost.html?post=${word.removePrefix("post://")}") - word.removePrefix("reply://").let(webInterface.core::getPostReply).isPresent -> redirect("viewPost.html?post=${word.removePrefix("reply://").let(webInterface.core::getPostReply).get().postId}") - word.removePrefix("album://").let(webInterface.core::getAlbum) != null -> redirect("imageBrowser.html?album=${word.removePrefix("album://")}") - word.removePrefix("image://").let { webInterface.core.getImage(it, false) } != null -> redirect("imageBrowser.html?image=${word.removePrefix("image://")}") + word.removePrefix("sone://").let(soneRequest.core::getSone) != null -> redirect("viewSone.html?sone=${word.removePrefix("sone://")}") + word.removePrefix("post://").let(soneRequest.core::getPost) != null -> redirect("viewPost.html?post=${word.removePrefix("post://")}") + word.removePrefix("reply://").let(soneRequest.core::getPostReply) != null -> redirect("viewPost.html?post=${word.removePrefix("reply://").let(soneRequest.core::getPostReply)?.postId}") + word.removePrefix("album://").let(soneRequest.core::getAlbum) != null -> redirect("imageBrowser.html?album=${word.removePrefix("album://")}") + word.removePrefix("image://").let { soneRequest.core.getImage(it, false) } != null -> redirect("imageBrowser.html?image=${word.removePrefix("image://")}") } } } - val sonePagination = webInterface.core.sones - .scoreAndPaginate(phrases) { it.allText() } - .apply { page = freenetRequest.parameters["sonePage"].emptyToNull?.toIntOrNull() ?: 0 } + val soneNameCache = { sone: Sone -> sone.names() }.memoize() + val sonePagination = soneRequest.core.sones + .scoreAndPaginate(phrases, soneRequest.core.preferences.postsPerPage) { it.allText(soneNameCache) } + .apply { page = soneRequest.parameters["sonePage"].emptyToNull?.toIntOrNull() ?: 0 } val postPagination = cache.get(phrases) { - webInterface.core.sones + soneRequest.core.sones .flatMap(Sone::getPosts) - .filter { Post.FUTURE_POSTS_FILTER.apply(it) } - .scoreAndPaginate(phrases) { it.allText() } - }.apply { page = freenetRequest.parameters["postPage"].emptyToNull?.toIntOrNull() ?: 0 } + .filter(noFuturePost) + .scoreAndPaginate(phrases, soneRequest.core.preferences.postsPerPage) { it.allText(soneNameCache, soneRequest.core::getReplies) } + }.apply { page = soneRequest.parameters["postPage"].emptyToNull?.toIntOrNull() ?: 0 } + Logger.normal(SearchPage::class.java, "Finished search for “${soneRequest.parameters["query"]}” in ${System.currentTimeMillis() - startTime}ms.") templateContext["sonePagination"] = sonePagination templateContext["soneHits"] = sonePagination.items templateContext["postPagination"] = postPagination templateContext["postHits"] = postPagination.items } - private fun Iterable.scoreAndPaginate(phrases: Iterable, texter: (T) -> String) = + private fun Iterable.scoreAndPaginate(phrases: Iterable, postsPerPage: Int, texter: (T) -> String) = map { it to score(texter(it), phrases) } .filter { it.second > 0 } .sortedByDescending { it.second } .map { it.first } - .paginate(webInterface.core.preferences.postsPerPage) + .paginate(postsPerPage) private fun Sone.names() = - listOf(name, profile.firstName, profile.middleName, profile.lastName) - .filterNotNull() - .joinToString("") + with(profile) { + listOf(name, firstName, middleName, lastName) + .filterNotNull() + .joinToString("") + } + + private fun Sone.allText(soneNameCache: (Sone) -> String) = + (soneNameCache(this) + profile.fields.map { "${it.name} ${it.value}" }.joinToString(" ", " ")).toLowerCase() - private fun Sone.allText() = - (names() + profile.fields.map { "${it.name} ${it.value}" }.joinToString(" ", " ")).toLowerCase() + private fun Post.allText(soneNameCache: (Sone) -> String, getReplies: (String) -> Collection) = + (text + recipient.orNull()?.let { " ${soneNameCache(it)}" } + getReplies(id) + .filter(noFutureReply) + .map { "${soneNameCache(it.sone)} ${it.text}" }.joinToString(" ", " ")).toLowerCase() - private fun Post.allText() = - (text + recipient.orNull()?.let { " ${it.names()}" } + webInterface.core.getReplies(id) - .filter { PostReply.FUTURE_REPLY_FILTER.apply(it) } - .map { "${it.sone.names()} ${it.text}" }.joinToString(" ", " ")).toLowerCase() + private fun Iterable.indicesFor(text: String, predicate: (Phrase) -> Boolean) = + filter(predicate).map(Phrase::phrase).map(String::toLowerCase).flatMap { text.findAll(it) } private fun score(text: String, phrases: Iterable): Double { val requiredPhrases = phrases.count { it.required } - val requiredHits = phrases.filter(Phrase::required) - .map(Phrase::phrase) - .flatMap { text.findAll(it) } + val requiredHits = phrases.indicesFor(text, Phrase::required) .map { Math.pow(1 - it / text.length.toDouble(), 2.0) } .sum() - val optionalHits = phrases.filter(Phrase::optional) - .map(Phrase::phrase) - .flatMap { text.findAll(it) } + val optionalHits = phrases.indicesFor(text, Phrase::optional) .map { Math.pow(1 - it / text.length.toDouble(), 2.0) } .sum() - val forbiddenHits = phrases.filter(Phrase::forbidden) - .map(Phrase::phrase) - .map { text.findAll(it).size } - .sum() + val forbiddenHits = phrases.indicesFor(text, Phrase::forbidden) + .count() return requiredHits * 3 + optionalHits + (requiredHits - requiredPhrases) * 5 - (forbiddenHits * 2) } - private fun String.findAll(needle: String): List { - var nextIndex = indexOf(needle) - val positions = mutableListOf() - while (nextIndex != -1) { - positions += nextIndex - nextIndex = indexOf(needle, nextIndex + 1) - } - return positions - } + private fun String.findAll(needle: String) = + generateSequence(indexOf(needle).takeIf { it > -1 }) { lastPosition -> + lastPosition + .let { indexOf(needle, it + 1) } + .takeIf { it > -1 } + }.toList() private fun String.parse() = StringEscaper.parseLine(this) - .map(String::toLowerCase) .map { when { it == "+" || it == "-" -> Phrase(it, OPTIONAL) @@ -128,7 +125,7 @@ class SearchPage @JvmOverloads constructor(template: Template, webInterface: Web } } - private fun redirect(target: String): Nothing = throw RedirectException(target) + private fun redirect(target: String): Nothing = redirectTo(target) enum class Optionality { OPTIONAL,