import javax.annotation.Nullable;
import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.core.ImageLoader;
+import net.pterodactylus.sone.core.ElementLoader;
import net.pterodactylus.sone.core.event.ImageInsertAbortedEvent;
import net.pterodactylus.sone.core.event.ImageInsertFailedEvent;
import net.pterodactylus.sone.core.event.ImageInsertFinishedEvent;
import net.pterodactylus.sone.template.ImageAccessor;
import net.pterodactylus.sone.template.ImageLinkFilter;
import net.pterodactylus.sone.template.JavascriptFilter;
-import net.pterodactylus.sone.template.LinkedImagesFilter;
+import net.pterodactylus.sone.template.LinkedElementRenderFilter;
+import net.pterodactylus.sone.template.LinkedElementsFilter;
import net.pterodactylus.sone.template.ParserFilter;
import net.pterodactylus.sone.template.PostAccessor;
import net.pterodactylus.sone.template.ProfileAccessor;
* The Sone plugin
*/
@Inject
- public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter, PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter, ImageLoader imageLoader) {
+ public WebInterface(SonePlugin sonePlugin, Loaders loaders, ListNotificationFilter listNotificationFilter, PostVisibilityFilter postVisibilityFilter, ReplyVisibilityFilter replyVisibilityFilter, ElementLoader elementLoader) {
this.sonePlugin = sonePlugin;
this.loaders = loaders;
this.listNotificationFilter = listNotificationFilter;
templateContextFactory.addFilter("parse", parserFilter = new ParserFilter(getCore(), soneTextParser));
templateContextFactory.addFilter("shorten", shortenFilter = new ShortenFilter());
templateContextFactory.addFilter("render", renderFilter = new RenderFilter(getCore(), templateContextFactory));
- templateContextFactory.addFilter("linked-images", new LinkedImagesFilter(imageLoader));
+ templateContextFactory.addFilter("linked-elements", new LinkedElementsFilter(elementLoader));
+ templateContextFactory.addFilter("render-linked-element", new LinkedElementRenderFilter(templateContextFactory));
templateContextFactory.addFilter("reparse", new ReparseFilter());
templateContextFactory.addFilter("unknown", new UnknownDateFilter(getL10n(), "View.Sone.Text.UnknownDate"));
templateContextFactory.addFilter("format", new FormatFilter());
--- /dev/null
+package net.pterodactylus.sone.core
+
+import com.google.common.cache.CacheBuilder
+import freenet.keys.FreenetURI
+import java.io.ByteArrayInputStream
+import javax.imageio.ImageIO
+import javax.inject.Inject
+
+/**
+ * [ElementLoader] implementation that uses a simple Guava [com.google.common.cache.Cache].
+ */
+class DefaultElementLoader @Inject constructor(private val freenetInterface: FreenetInterface) : ElementLoader {
+
+ private val loadingLinks = CacheBuilder.newBuilder().build<String, Boolean>()
+ private val imageCache = CacheBuilder.newBuilder().build<String, LinkedImage>()
+ private val callback = object : FreenetInterface.BackgroundFetchCallback {
+ override fun loaded(uri: FreenetURI, mimeType: String, data: ByteArray) {
+ if (!mimeType.startsWith("image/")) {
+ return
+ }
+ ByteArrayInputStream(data).use {
+ ImageIO.read(it)
+ }?.let {
+ imageCache.get(uri.toString()) { LinkedImage(uri.toString()) }
+ }
+ removeLoadingLink(uri)
+ }
+
+ override fun failed(uri: FreenetURI) {
+ removeLoadingLink(uri)
+ }
+
+ private fun removeLoadingLink(uri: FreenetURI) {
+ synchronized(loadingLinks) {
+ loadingLinks.invalidate(uri.toString())
+ }
+ }
+ }
+
+ override fun loadElement(link: String): LinkedElement {
+ synchronized(loadingLinks) {
+ imageCache.getIfPresent(link)?.run {
+ return this
+ }
+ if (loadingLinks.getIfPresent(link) == null) {
+ loadingLinks.put(link, true)
+ freenetInterface.startFetch(FreenetURI(link), callback)
+ }
+ }
+ return object : LinkedElement {
+ override val link = link
+ override val loading = true
+ }
+ }
+
+}
+++ /dev/null
-package net.pterodactylus.sone.core
-
-import com.google.common.cache.CacheBuilder
-import freenet.keys.FreenetURI
-import java.io.ByteArrayInputStream
-import javax.imageio.ImageIO
-import javax.inject.Inject
-
-/**
- * [ImageLoader] implementation that uses a simple Guava [com.google.common.cache.Cache].
- */
-class DefaultImageLoader @Inject constructor(private val freenetInterface: FreenetInterface) : ImageLoader {
-
- private val imageCache = CacheBuilder.newBuilder().build<String, LoadedImage>()
- private val callback = object : FreenetInterface.BackgroundFetchCallback {
- override fun loaded(uri: FreenetURI, mimeType: String, data: ByteArray) {
- if (!mimeType.startsWith("image/")) {
- return
- }
- val image = ByteArrayInputStream(data).use {
- ImageIO.read(it)
- }
- val loadedImage = LoadedImage(uri.toString(), mimeType, image.width, image.height)
- imageCache.get(uri.toString()) { loadedImage }
- }
-
- override fun failed(uri: FreenetURI) {
- }
- }
-
- override fun toLoadedImage(link: String): LoadedImage? {
- imageCache.getIfPresent(link)?.run {
- return this
- }
- freenetInterface.startFetch(FreenetURI(link), callback)
- return null
- }
-
-}
--- /dev/null
+package net.pterodactylus.sone.core
+
+import com.google.inject.ImplementedBy
+
+/**
+ * Component that loads images and supplies information about them.
+ */
+@ImplementedBy(DefaultElementLoader::class)
+interface ElementLoader {
+
+ fun loadElement(link: String): LinkedElement
+
+}
+
+interface LinkedElement {
+
+ val link: String
+ val loading: Boolean
+
+}
+
+data class LinkedImage(override val link: String, override val loading: Boolean = false) : LinkedElement
+++ /dev/null
-package net.pterodactylus.sone.core
-
-import com.google.inject.ImplementedBy
-
-/**
- * Component that loads images and supplies information about them.
- */
-@ImplementedBy(DefaultImageLoader::class)
-interface ImageLoader {
-
- fun toLoadedImage(link: String): LoadedImage?
-
-}
-
-data class LoadedImage(val link: String, val mimeType: String, val width: Int, val height: Int)
--- /dev/null
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.LinkedElement
+import net.pterodactylus.sone.core.LinkedImage
+import net.pterodactylus.util.template.Filter
+import net.pterodactylus.util.template.TemplateContext
+import net.pterodactylus.util.template.TemplateContextFactory
+import net.pterodactylus.util.template.TemplateParser
+import java.io.StringReader
+import java.io.StringWriter
+
+/**
+ * Renders all kinds of [LinkedElement]s.
+ */
+class LinkedElementRenderFilter(private val templateContextFactory: TemplateContextFactory) : Filter {
+
+ companion object {
+ private val loadedImageTemplate = """<a href="/<% link|html>"><span class="linked-element" title="<% link|html>" style="background-image: url('/<% link|html>')"></span></a>""".parse()
+ private val notLoadedImageTemplate = """<span class="linked-element" title="<% link|html>" style="background-image: url('images/loading-animation.gif')"></span>""".parse()
+
+ private fun String.parse() = StringReader(this).use { TemplateParser.parse(it) }
+ }
+
+ override fun format(templateContext: TemplateContext?, data: Any?, parameters: Map<String, Any?>?) =
+ when {
+ data is LinkedElement && data.loading -> renderNotLoadedLinkedElement(data)
+ data is LinkedImage -> renderLinkedImage(data)
+ else -> null
+ }
+
+ private fun renderLinkedImage(linkedImage: LinkedImage) =
+ StringWriter().use {
+ val templateContext = templateContextFactory.createTemplateContext()
+ templateContext["link"] = linkedImage.link
+ loadedImageTemplate.render(templateContext, it)
+ it
+ }.toString()
+
+ private fun renderNotLoadedLinkedElement(linkedElement: LinkedElement) =
+ StringWriter().use {
+ val templateContext = templateContextFactory.createTemplateContext()
+ templateContext["link"] = linkedElement.link
+ notLoadedImageTemplate.render(templateContext, it)
+ it
+ }.toString()
+
+}
--- /dev/null
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.ElementLoader
+import net.pterodactylus.sone.core.LinkedElement
+import net.pterodactylus.sone.text.FreenetLinkPart
+import net.pterodactylus.sone.text.Part
+import net.pterodactylus.util.template.Filter
+import net.pterodactylus.util.template.TemplateContext
+
+/**
+ * Filter that takes a number of pre-rendered [Part]s and replaces all identified links to freenet elements
+ * with [LinkedElement]s.
+ */
+class LinkedElementsFilter(private val elementLoader: ElementLoader) : Filter {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun format(templateContext: TemplateContext?, data: Any?, parameters: MutableMap<String, Any?>?) =
+ (data as? Iterable<Part>)
+ ?.filterIsInstance<FreenetLinkPart>()
+ ?.map { elementLoader.loadElement(it.link) }
+ ?: listOf<LinkedElement>()
+
+}
+++ /dev/null
-package net.pterodactylus.sone.template
-
-import net.pterodactylus.sone.core.ImageLoader
-import net.pterodactylus.sone.core.LoadedImage
-import net.pterodactylus.sone.text.FreenetLinkPart
-import net.pterodactylus.sone.text.Part
-import net.pterodactylus.util.template.Filter
-import net.pterodactylus.util.template.TemplateContext
-
-/**
- * Filter that takes a number of pre-rendered [Part]s and replaces all identified links to freenet images
- * with [LoadedImage]s.
- */
-class LinkedImagesFilter(private val imageLoader: ImageLoader) : Filter {
-
- @Suppress("UNCHECKED_CAST")
- override fun format(templateContext: TemplateContext?, data: Any?, parameters: MutableMap<String, Any?>?) =
- (data as? Iterable<Part>)
- ?.filterIsInstance<FreenetLinkPart>()
- ?.mapNotNull { imageLoader.toLoadedImage(it.link) }
- ?: listOf<LoadedImage>()
-
-}
color: green;
}
-#sone .post .linked-images {
+#sone .post .linked-elements {
margin-top: 1ex;
}
-#sone .post .linked-image {
+#sone .post .linked-element {
display: inline-block;
border: solid 1px black;
width: 160px;
font-size: inherit;
}
-#sone .post .reply .linked-images {
+#sone .post .reply .linked-elements {
margin-top: 1ex;
}
-#sone .post .reply .linked-image {
+#sone .post .reply .linked-element {
display: inline-block;
border: solid 1px black;
width: 120px;
<div class="post-text short-text<%if raw> hidden<%/if><%if shortText|match key=renderedText> hidden<%/if>"><% shortText></div>
<%if !shortText|match value=renderedText><%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 value=renderedText><%if !raw><a class="shrink-post-text hidden"><%= View.Post.ShowLess|l10n|html></a><%/if><%/if>
- <% parsedText|linked-images|store key==linkedImages>
- <% foreach linkedImages linkedImage>
+ <% parsedText|linked-elements|store key==linkedElements>
+ <% foreach linkedElements linkedElement>
<% first>
- <div class="linked-images">
+ <div class="linked-elements">
<%/first>
- <a href="/<% linkedImage.link|html>">
- <span class="linked-image" title="<% linkedImage.link|html>" style="background-image: url('/<% linkedImage.link|html>')"/>
- </a>
+ <% linkedElement|render-linked-element>
<% last>
</div>
<%/last>
<div class="reply-text short-text<%if raw> hidden<%/if><%if shortText|match key=renderedText> hidden<%/if>"><% shortText></div>
<%if !shortText|match value=renderedText><%if !raw><a class="expand-reply-text" href="viewPost.html?post=<% reply.postId|html>&raw=true"><%= View.Post.ShowMore|l10n|html></a><%/if><%/if>
<%if !shortText|match value=renderedText><%if !raw><a class="shrink-reply-text hidden"><%= View.Post.ShowLess|l10n|html></a><%/if><%/if>
- <% parsedText|linked-images|store key==linkedImages>
- <% foreach linkedImages linkedImage>
+ <% parsedText|linked-elements|store key==linkedElements>
+ <% foreach linkedElements linkedElement>
<% first>
- <div class="linked-images">
+ <div class="linked-elements">
<%/first>
- <a href="/<% linkedImage.link|html>">
- <span class="linked-image" title="<% linkedImage.link|html>" style="background-image: url('/<% linkedImage.link|html>')"/>
- </a>
+ <% linkedElement|render-linked-element>
<% last>
</div>
<%/last>
--- /dev/null
+package net.pterodactylus.sone.core
+
+import com.google.common.io.ByteStreams
+import com.google.common.io.Files
+import freenet.keys.FreenetURI
+import net.pterodactylus.sone.core.FreenetInterface.BackgroundFetchCallback
+import net.pterodactylus.sone.test.capture
+import net.pterodactylus.sone.test.mock
+import org.hamcrest.MatcherAssert
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers
+import org.hamcrest.Matchers.`is`
+import org.hamcrest.Matchers.instanceOf
+import org.hamcrest.Matchers.nullValue
+import org.junit.Test
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.io.ByteArrayOutputStream
+
+/**
+ * Unit test for [DefaultElementLoaderTest].
+ */
+class DefaultElementLoaderTest {
+
+ companion object {
+ private const val IMAGE_ID = "KSK@gpl.png"
+ }
+
+ private val freenetInterface = mock<FreenetInterface>()
+ private val elementLoader = DefaultElementLoader(freenetInterface)
+ private val callback = capture<BackgroundFetchCallback>()
+
+ @Test
+ fun `image loader starts request for link that is not known`() {
+ elementLoader.loadElement(IMAGE_ID)
+ verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), any<BackgroundFetchCallback>())
+ }
+
+ @Test
+ fun `element loader only starts request once`() {
+ elementLoader.loadElement(IMAGE_ID)
+ elementLoader.loadElement(IMAGE_ID)
+ verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), any<BackgroundFetchCallback>())
+ }
+
+ @Test
+ fun `element loader returns loading element on first call`() {
+ assertThat(elementLoader.loadElement(IMAGE_ID).loading, `is`(true))
+ }
+
+ @Test
+ fun `image loader can load image`() {
+ elementLoader.loadElement(IMAGE_ID)
+ verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), callback.capture())
+ callback.value.loaded(FreenetURI(IMAGE_ID), "image/png", read("/static/images/unknown-image-0.png"))
+ val linkedElement = elementLoader.loadElement(IMAGE_ID)
+ assertThat(linkedElement.link, `is`(IMAGE_ID))
+ assertThat(linkedElement.loading, `is`(false))
+ assertThat(linkedElement, instanceOf(LinkedImage::class.java))
+ }
+
+ @Test
+ fun `image can be loaded again after it failed`() {
+ elementLoader.loadElement(IMAGE_ID)
+ verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), callback.capture())
+ callback.value.failed(FreenetURI(IMAGE_ID))
+ elementLoader.loadElement(IMAGE_ID)
+ verify(freenetInterface, times(2)).startFetch(eq(FreenetURI(IMAGE_ID)), callback.capture())
+ }
+
+ private fun read(resource: String): ByteArray =
+ javaClass.getResourceAsStream(resource)?.use { input ->
+ ByteArrayOutputStream().use {
+ ByteStreams.copy(input, it)
+ it
+ }.toByteArray()
+ } ?: ByteArray(0)
+
+}
+++ /dev/null
-package net.pterodactylus.sone.core
-
-import com.google.common.io.ByteStreams
-import com.google.common.io.Files
-import freenet.keys.FreenetURI
-import net.pterodactylus.sone.core.FreenetInterface.BackgroundFetchCallback
-import net.pterodactylus.sone.test.capture
-import net.pterodactylus.sone.test.mock
-import org.hamcrest.MatcherAssert
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers
-import org.hamcrest.Matchers.`is`
-import org.hamcrest.Matchers.nullValue
-import org.junit.Test
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.eq
-import org.mockito.Mockito.verify
-import java.io.ByteArrayOutputStream
-
-/**
- * Unit test for [DefaultImageLoaderTest].
- */
-class DefaultImageLoaderTest {
-
- companion object {
- private const val IMAGE_ID = "KSK@gpl.png"
- }
-
- private val freenetInterface = mock<FreenetInterface>()
- private val imageLoader = DefaultImageLoader(freenetInterface)
- private val callback = capture<BackgroundFetchCallback>()
-
- @Test
- fun `image loader starts request for link that is not known`() {
- assertThat(imageLoader.toLoadedImage(IMAGE_ID), nullValue())
- verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), any<BackgroundFetchCallback>())
- }
-
- @Test
- fun `image loader can load image`() {
- assertThat(imageLoader.toLoadedImage(IMAGE_ID), nullValue())
- verify(freenetInterface).startFetch(eq(FreenetURI(IMAGE_ID)), callback.capture())
- callback.value.loaded(FreenetURI(IMAGE_ID), "image/png", read("/static/images/unknown-image-0.png"))
- val loadedImage = imageLoader.toLoadedImage(IMAGE_ID)!!
- assertThat(loadedImage.link, `is`(IMAGE_ID))
- assertThat(loadedImage.mimeType, `is`("image/png"))
- assertThat(loadedImage.width, `is`(200))
- assertThat(loadedImage.height, `is`(150))
- }
-
- private fun read(resource: String): ByteArray =
- javaClass.getResourceAsStream(resource)?.use { input ->
- ByteArrayOutputStream().use {
- ByteStreams.copy(input, it)
- it
- }.toByteArray()
- } ?: ByteArray(0)
-
-}
--- /dev/null
+package net.pterodactylus.sone.core
+
+import com.google.inject.Guice.createInjector
+import net.pterodactylus.sone.test.bindMock
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.notNullValue
+import org.junit.Test
+
+/**
+ * Unit test for [ElementLoader].
+ */
+class ElementLoaderTest {
+
+ @Test
+ fun `default image loader can be loaded by guice`() {
+ val injector = createInjector(bindMock<FreenetInterface>())
+ assertThat(injector.getInstance(ElementLoader::class.java), notNullValue());
+ }
+
+}
+++ /dev/null
-package net.pterodactylus.sone.core
-
-import com.google.inject.Guice.createInjector
-import net.pterodactylus.sone.test.bindMock
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.notNullValue
-import org.junit.Test
-
-/**
- * Unit test for [ImageLoader].
- */
-class ImageLoaderTest {
-
- @Test
- fun `default image loader can be loaded by guice`() {
- val injector = createInjector(bindMock<FreenetInterface>())
- assertThat(injector.getInstance(ImageLoader::class.java), notNullValue());
- }
-
-}
--- /dev/null
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.LinkedImage
+import net.pterodactylus.util.template.HtmlFilter
+import net.pterodactylus.util.template.TemplateContextFactory
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.`is`
+import org.jsoup.Jsoup
+import org.junit.Test
+
+/**
+ * Unit test for [LinkedElementRenderFilter].
+ */
+class LinkedElementRenderFilterTest {
+
+ private val templateContextFactory = TemplateContextFactory()
+
+ init {
+ templateContextFactory.addFilter("html", HtmlFilter())
+ }
+
+ private val filter = LinkedElementRenderFilter(templateContextFactory)
+
+ @Test
+ fun `filter can render linked images`() {
+ val html = filter.format(null, LinkedImage("KSK@gpl.png"), emptyMap<String, Any?>()) as String
+ val linkNode = Jsoup.parseBodyFragment(html).body().child(0)
+ assertThat(linkNode.nodeName(), `is`("a"))
+ assertThat(linkNode.attr("href"), `is`("/KSK@gpl.png"))
+ val spanNode = linkNode.child(0)
+ assertThat(spanNode.nodeName(), `is`("span"))
+ assertThat(spanNode.attr("class"), `is`("linked-element"))
+ assertThat(spanNode.attr("title"), `is`("KSK@gpl.png"))
+ assertThat(spanNode.attr("style"), `is`("background-image: url('/KSK@gpl.png')"))
+ }
+
+}
--- /dev/null
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.ElementLoader
+import net.pterodactylus.sone.core.LinkedElement
+import net.pterodactylus.sone.core.LinkedImage
+import net.pterodactylus.sone.test.mock
+import net.pterodactylus.sone.text.FreenetLinkPart
+import net.pterodactylus.sone.text.LinkPart
+import net.pterodactylus.sone.text.PlainTextPart
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.junit.Test
+import org.mockito.Mockito.`when`
+
+/**
+ * Unit test for [LinkedElementsFilter].
+ */
+class LinkedElementsFilterTest {
+
+ private val imageLoader = mock<ElementLoader>()
+ private val filter = LinkedElementsFilter(imageLoader)
+
+ @Test
+ fun `filter finds all loaded freenet images`() {
+ val parts = listOf(
+ PlainTextPart("text"),
+ LinkPart("http://link", "link"),
+ FreenetLinkPart("KSK@link", "link", false),
+ FreenetLinkPart("KSK@link.png", "link", false)
+ )
+ `when`(imageLoader.loadElement("KSK@link")).thenReturn(LinkedImage("KSK@link", true))
+ `when`(imageLoader.loadElement("KSK@link.png")).thenReturn(LinkedImage("KSK@link.png"))
+ val loadedImages = filter.format(null, parts, null)
+ assertThat(loadedImages, contains<LinkedElement>(
+ LinkedImage("KSK@link", true),
+ LinkedImage("KSK@link.png")
+ ))
+ }
+
+}
+++ /dev/null
-package net.pterodactylus.sone.template
-
-import net.pterodactylus.sone.core.ImageLoader
-import net.pterodactylus.sone.core.LoadedImage
-import net.pterodactylus.sone.test.mock
-import net.pterodactylus.sone.text.FreenetLinkPart
-import net.pterodactylus.sone.text.LinkPart
-import net.pterodactylus.sone.text.PlainTextPart
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers
-import org.junit.Test
-import org.mockito.Mockito.`when`
-
-/**
- * Unit test for [LinkedImagesFilter].
- */
-class LinkedImagesFilterTest {
-
- private val imageLoader = mock<ImageLoader>()
- private val filter = LinkedImagesFilter(imageLoader)
-
- @Test
- fun `filter finds all loaded freenet images`() {
- val parts = listOf(
- PlainTextPart("text"),
- LinkPart("http://link", "link"),
- FreenetLinkPart("KSK@link", "link", false),
- FreenetLinkPart("KSK@link.png", "link", false)
- )
- `when`(imageLoader.toLoadedImage("KSK@link.png")).thenReturn(LoadedImage("KSK@link.png", "image/png", 1440, 900))
- val loadedImages = filter.format(null, parts, null)
- assertThat(loadedImages, Matchers.contains(
- LoadedImage("KSK@link.png", "image/png", 1440, 900)
- ))
- }
-
-}