Content of file DefaultBlogViewController.java.html

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>DefaultBlogViewController.java</title><link rel="stylesheet" href="../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">NorthernWind :: Frontend :: Components</a> &gt; <a href="index.source.html" class="el_package">it.tidalwave.northernwind.frontend.ui.component.blog</a> &gt; <span class="el_source">DefaultBlogViewController.java</span></div><h1>DefaultBlogViewController.java</h1><pre class="source lang-java linenums">/*
 * #%L
 * *********************************************************************************************************************
 *
 * NorthernWind - lightweight CMS
 * http://northernwind.tidalwave.it - git clone https://bitbucket.org/tidalwave/northernwind-src.git
 * %%
 * Copyright (C) 2011 - 2023 Tidalwave s.a.s. (http://tidalwave.it)
 * %%
 * *********************************************************************************************************************
 *
 * Licensed under the Apache License, Version 2.0 (the &quot;License&quot;); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an &quot;AS IS&quot; BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations under the License.
 *
 * *********************************************************************************************************************
 *
 *
 * *********************************************************************************************************************
 * #L%
 */
package it.tidalwave.northernwind.frontend.ui.component.blog;

import javax.annotation.Nonnull;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import it.tidalwave.util.Finder;
import it.tidalwave.util.Key;
import it.tidalwave.util.spi.HierarchicFinderSupport;
import it.tidalwave.northernwind.core.model.Content;
import it.tidalwave.northernwind.core.model.HttpStatusException;
import it.tidalwave.northernwind.core.model.RequestLocaleManager;
import it.tidalwave.northernwind.core.model.ResourcePath;
import it.tidalwave.northernwind.core.model.ResourceProperties;
import it.tidalwave.northernwind.core.model.SiteNode;
import it.tidalwave.northernwind.frontend.ui.RenderContext;
import it.tidalwave.northernwind.frontend.ui.spi.VirtualSiteNode;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.With;
import lombok.extern.slf4j.Slf4j;
import static java.util.Collections.reverseOrder;
import static java.util.Collections.*;
import static java.util.Comparator.*;
import static java.util.stream.Collectors.*;
import static javax.servlet.http.HttpServletResponse.*;
import static it.tidalwave.util.CollectionUtils.split;
import static it.tidalwave.util.LocalizedDateTimeFormatters.getDateTimeFormatterFor;
import static it.tidalwave.northernwind.core.model.Content.*;
import static it.tidalwave.northernwind.frontend.ui.component.Properties.*;
import static it.tidalwave.northernwind.frontend.ui.component.nodecontainer.NodeContainerViewController.*;
import static it.tidalwave.northernwind.util.UrlEncoding.encodedUtf8;
import static lombok.AccessLevel.PUBLIC;

/***********************************************************************************************************************
 *
 * &lt;p&gt;A default implementation of the {@link BlogViewController} that is independent of the presentation technology.
 * This class is capable to render:&lt;/p&gt;
 *
 * &lt;ul&gt;
 * &lt;li&gt;blog posts (in various ways)&lt;/li&gt;
 * &lt;li&gt;an index of the blog&lt;/li&gt;
 * &lt;li&gt;a tag cloud&lt;/li&gt;
 * &lt;/ul&gt;
 *
 * &lt;p&gt;It accepts path parameters as follows:&lt;/p&gt;
 *
 * &lt;ul&gt;
 * &lt;li&gt;{@code &lt;uri&gt;}: selects a single post with the given uri;&lt;/li&gt;
 * &lt;li&gt;{@code &lt;category&gt;}: selects posts with the given category;&lt;/li&gt;
 * &lt;li&gt;{@code tags/&lt;tag&gt;}: selects posts with the given tag;&lt;/li&gt;
 * &lt;li&gt;{@code index}: renders a post index, with links to single posts;&lt;/li&gt;
 * &lt;li&gt;{@code index/&lt;category&gt;}: renders an index of posts with the given category;&lt;/li&gt;
 * &lt;li&gt;{@code index/tag/&lt;tag&gt;}: renders an index of posts with the given tag.&lt;/li&gt;
 * &lt;/ul&gt;
 *
 * &lt;p&gt;Supported properties of the {@link SiteNode}:&lt;/p&gt;
 *
 * &lt;ul&gt;
 * &lt;li&gt;{@code P_CONTENT_PATHS}: one or more {@code Content} that contains the posts to render; they are folders and can have
 *     sub-folders, which will be searched for in a recursive fashion;&lt;/li&gt;
 * &lt;li&gt;{@code P_MAX_FULL_ITEMS}: the max. number of posts to be rendered in full;&lt;/li&gt;
 * &lt;li&gt;{@code P_MAX_LEADIN_ITEMS}: the max. number of posts to be rendered with lead-in text;&lt;/li&gt;
 * &lt;li&gt;{@code P_MAX_ITEMS}: the max. number of posts to be rendered as links;&lt;/li&gt;
 * &lt;li&gt;{@code P_DATE_FORMAT}: the pattern for formatting date and times;&lt;/li&gt;
 * &lt;li&gt;{@code P_TIME_ZONE}: the time zone for rendering dates (defaults to CET);&lt;/li&gt;
 * &lt;li&gt;{@code P_INDEX}: if {@code true}, forces an index rendering (useful e.g. when used in sidebars);&lt;/li&gt;
 * &lt;li&gt;{@code P_TAG_CLOUD}: if {@code true}, forces a tag cloud rendering (useful e.g. when used in sidebars).&lt;/li&gt;
 * &lt;/ul&gt;
 *
 * &lt;p&gt;The {@code P_DATE_FORMAT} property accepts any valid pattern in Java 8, plus the values {@code S-}, {@code M-},
 * {@code L-}, {@code F-}, which stand for small/medium/large and full patterns for a given locale.&lt;/p&gt;
 *
 * &lt;p&gt;Supported properties of the {@link Content}:&lt;/p&gt;
 *
 * &lt;ul&gt;
 * &lt;li&gt;{@code P_TITLE}: the title;&lt;/li&gt;
 * &lt;li&gt;{@code P_FULL_TEXT}: the full text;&lt;/li&gt;
 * &lt;li&gt;{@code P_LEADIN_TEXT}: the lead-in text;&lt;/li&gt;
 * &lt;li&gt;{@code P_ID}: the unique id;&lt;/li&gt;
 * &lt;li&gt;{@code P_IMAGE_ID}: the id of an image representative of the post;&lt;/li&gt;
 * &lt;li&gt;{@code P_PUBLISHING_DATE}: the publishing date;&lt;/li&gt;
 * &lt;li&gt;{@code P_CREATION_DATE}: the creation date;&lt;/li&gt;
 * &lt;li&gt;{@code P_TAGS}: the tags;&lt;/li&gt;
 * &lt;li&gt;{@code P_CATEGORY}: the category.&lt;/li&gt;
 * &lt;/ul&gt;
 *
 * &lt;p&gt;When preparing for rendering, the following dynamic properties will be set, only if a single post is rendered:&lt;/p&gt;
 *
 * &lt;ul&gt;
 * &lt;li&gt;{@code PD_URL}: the canonical URL of the post;&lt;/li&gt;
 * &lt;li&gt;{@code PD_ID}: the unique id of the post;&lt;/li&gt;
 * &lt;li&gt;{@code PD_IMAGE_ID}: the id of the representative image.&lt;/li&gt;
 * &lt;/ul&gt;
 *
 * &lt;p&gt;Concrete implementations must provide two methods for rendering the blog posts and the tag cloud:&lt;/p&gt;
 *
 * &lt;ul&gt;
 * &lt;li&gt;{@link #renderPosts(java.util.List, java.util.List, java.util.List) }&lt;/li&gt;
 * &lt;li&gt;{@link #renderTagCloud(java.util.Collection)  }&lt;/li&gt;
 * &lt;/ul&gt;
 *
 * @author  Fabrizio Giudici
 *
 **********************************************************************************************************************/
<span class="fc" id="L145">@RequiredArgsConstructor @Slf4j</span>
public abstract class DefaultBlogViewController implements BlogViewController
  {
    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
<span class="pc bpc" id="L152" title="16 of 24 branches missed.">    @AllArgsConstructor(access = PUBLIC) @Getter @EqualsAndHashCode</span>
    public static class TagAndCount
      {
<span class="fc" id="L155">        public final String tag;</span>
<span class="fc" id="L156">        public final int count;</span>

<span class="pc bpc" id="L158" title="1 of 2 branches missed.">        @With</span>
<span class="fc" id="L159">        public final String rank;</span>

        public TagAndCount (@Nonnull final String tag)
          {
<span class="fc" id="L163">            this(tag, 1, &quot;&quot;);</span>
<span class="fc" id="L164">          }</span>

        @Nonnull
        public TagAndCount reduced (@Nonnull final TagAndCount other)
          {
<span class="pc bpc" id="L169" title="1 of 2 branches missed.">            if (!this.tag.equals(other.tag))</span>
              {
<span class="nc" id="L171">                throw new IllegalArgumentException(&quot;Mismatching &quot; + this + &quot; vs &quot; + other);</span>
              }

<span class="fc" id="L174">            return new TagAndCount(tag, this.count + other.count, &quot;&quot;);</span>
          }

        @Override @Nonnull
        public String toString()
          {
<span class="fc" id="L180">            return String.format(&quot;TagAndCount(%s, %d, %s)&quot;, tag, count, rank);</span>
          }
      }

    /*******************************************************************************************************************
     *
     * A {@link Finder} which returns virtual {@link SiteNode}s representing the multiple contents served by the
     * {@link SiteNode} associated to this controller. This is typically used to create site maps.
     *
     ******************************************************************************************************************/
    // TODO: add eventual localized versions
<span class="fc" id="L191">    @RequiredArgsConstructor</span>
    private static class VirtualSiteNodeFinder extends HierarchicFinderSupport&lt;SiteNode, VirtualSiteNodeFinder&gt;
      {
        private static final long serialVersionUID = 1L;

        @Nonnull
        private final transient DefaultBlogViewController controller;

        public VirtualSiteNodeFinder (@Nonnull final VirtualSiteNodeFinder other, @Nonnull final Object override)
          {
<span class="nc" id="L201">            super(other, override);</span>
<span class="nc" id="L202">            final var source = getSource(VirtualSiteNodeFinder.class, other, override);</span>
<span class="nc" id="L203">            this.controller = source.controller;</span>
<span class="nc" id="L204">          }</span>

        @Override @Nonnull
        protected List&lt;SiteNode&gt; computeResults()
          {
<span class="fc" id="L209">            return controller.findAllPosts(controller.getViewProperties())</span>
<span class="fc" id="L210">                             .stream()</span>
<span class="fc" id="L211">                             .peek(p -&gt; log.trace(&quot;&gt;&gt;&gt;&gt; virtual node for: {}&quot;, p.getExposedUri()))</span>
<span class="fc" id="L212">                             .flatMap(post -&gt; createVirtualNode(post).stream())</span>
<span class="fc" id="L213">                             .collect(toList());</span>
          }

        @Nonnull
        private Optional&lt;VirtualSiteNode&gt; createVirtualNode (@Nonnull final Content post)
          {
<span class="fc" id="L219">            final var siteNode = controller.siteNode;</span>
<span class="fc" id="L220">            return post.getExposedUri().map(uri -&gt; new VirtualSiteNode(siteNode,</span>
<span class="fc" id="L221">                                                                       siteNode.getRelativeUri().appendedWith(uri),</span>
<span class="fc" id="L222">                                                                       post.getProperties()));</span>
          }
      }

<span class="fc" id="L226">    private static final Map&lt;String, Function&lt;Locale, DateTimeFormatter&gt;&gt; DATETIME_FORMATTER_MAP_BY_STYLE = new HashMap&lt;&gt;();</span>

    static
      {
<span class="fc" id="L230">        DATETIME_FORMATTER_MAP_BY_STYLE.put(&quot;S-&quot;, locale -&gt; getDateTimeFormatterFor(FormatStyle.SHORT, locale));</span>
<span class="fc" id="L231">        DATETIME_FORMATTER_MAP_BY_STYLE.put(&quot;M-&quot;, locale -&gt; getDateTimeFormatterFor(FormatStyle.MEDIUM, locale));</span>
<span class="fc" id="L232">        DATETIME_FORMATTER_MAP_BY_STYLE.put(&quot;L-&quot;, locale -&gt; getDateTimeFormatterFor(FormatStyle.LONG, locale));</span>
<span class="fc" id="L233">        DATETIME_FORMATTER_MAP_BY_STYLE.put(&quot;F-&quot;, locale -&gt; getDateTimeFormatterFor(FormatStyle.FULL, locale));</span>
      }

<span class="fc" id="L236">    protected static final List&lt;Key&lt;ZonedDateTime&gt;&gt; DATE_KEYS = List.of(P_PUBLISHING_DATE, P_CREATION_DATE);</span>

<span class="fc" id="L238">    public static final ZonedDateTime TIME0 = Instant.ofEpochMilli(0).atZone(ZoneId.of(&quot;GMT&quot;));</span>

    public static final String DEFAULT_TIMEZONE = &quot;CET&quot;;

    private static final int NO_LIMIT = 9999;

    private static final String INDEX_PREFIX = &quot;index&quot;;

    private static final String TAG_PREFIX = &quot;tag&quot;;

<span class="fc" id="L248">    private static final ResourcePath TAG_CLOUD = ResourcePath.of(&quot;tags&quot;);</span>

<span class="fc" id="L250">    private static final Comparator&lt;Content&gt; REVERSE_DATE_COMPARATOR = (p1, p2) -&gt;</span>
<span class="fc" id="L251">        p2.getProperty(DATE_KEYS).orElse(TIME0).compareTo(p1.getProperty(DATE_KEYS).orElse(TIME0));</span>

    @Nonnull
    private final SiteNode siteNode;

    @Nonnull
    private final BlogView view;

    @Nonnull
    private final RequestLocaleManager requestLocaleManager;

<span class="fc" id="L262">    private Optional&lt;String&gt; tag = Optional.empty();</span>

<span class="fc" id="L264">    private Optional&lt;String&gt; uriOrCategory = Optional.empty();</span>

    private boolean indexMode;

    private boolean tagCloudMode;

<span class="fc" id="L270">    protected Optional&lt;String&gt; title = Optional.empty();</span>

<span class="fc" id="L272">    /* VisibleForTesting */ final List&lt;Content&gt; fullPosts = new ArrayList&lt;&gt;();</span>

<span class="fc" id="L274">    /* VisibleForTesting */ final List&lt;Content&gt; leadInPosts = new ArrayList&lt;&gt;();</span>

<span class="fc" id="L276">    /* VisibleForTesting */ final List&lt;Content&gt; linkedPosts = new ArrayList&lt;&gt;();</span>

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override
    public void prepareRendering (@Nonnull final RenderContext context)
      throws HttpStatusException
      {
<span class="fc" id="L287">        log.info(&quot;prepareRendering(RenderContext) for {}&quot;, siteNode);</span>

<span class="fc" id="L289">        final var viewProperties = getViewProperties();</span>
<span class="fc" id="L290">        indexMode  = viewProperties.getProperty(P_INDEX).orElse(false);</span>
<span class="fc" id="L291">        var pathParams = context.getPathParams(siteNode);</span>
<span class="fc" id="L292">        tagCloudMode = viewProperties.getProperty(P_TAG_CLOUD).orElse(false);</span>

<span class="fc bfc" id="L294" title="All 2 branches covered.">        if (pathParams.equals(TAG_CLOUD))</span>
          {
<span class="fc" id="L296">            tagCloudMode = true;</span>
          }
<span class="fc bfc" id="L298" title="All 2 branches covered.">        else if (pathParams.startsWith(INDEX_PREFIX))</span>
          {
<span class="fc" id="L300">            indexMode = true;</span>
<span class="fc" id="L301">            pathParams = pathParams.withoutLeading();</span>
          }

<span class="fc bfc" id="L304" title="All 4 branches covered.">        if (pathParams.startsWith(TAG_PREFIX) &amp;&amp; (pathParams.getSegmentCount() == 2)) // matches(TAG_PREFIX, &quot;.*&quot;)</span>
          {
<span class="fc" id="L306">            tag = Optional.of(pathParams.getTrailing());</span>
          }
<span class="fc bfc" id="L308" title="All 2 branches covered.">        else if (pathParams.getSegmentCount() == 1)</span>
          {
<span class="fc" id="L310">            uriOrCategory = Optional.of(pathParams.getLeading());</span>
          }
<span class="fc bfc" id="L312" title="All 2 branches covered.">        else if (!pathParams.isEmpty())</span>
          {
<span class="fc" id="L314">            throw new HttpStatusException(SC_BAD_REQUEST);</span>
          }

<span class="fc bfc" id="L317" title="All 2 branches covered.">        if (tagCloudMode)</span>
          {
<span class="fc" id="L319">            setTitle(context);</span>
          }
        else
          {
<span class="fc" id="L323">            prepareBlogPosts(context, viewProperties);</span>

<span class="pc bpc" id="L325" title="2 of 6 branches missed.">            if ((fullPosts.size() == 1) &amp;&amp; leadInPosts.isEmpty() &amp;&amp; linkedPosts.isEmpty())</span>
              {
<span class="fc" id="L327">                setDynamicProperties(context, fullPosts.get(0));</span>
              }
            else
              {
<span class="fc" id="L331">                setTitle(context);</span>
              }
          }
<span class="fc" id="L334">      }</span>

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override
    public void renderView (@Nonnull final RenderContext context)
      throws Exception
      {
<span class="fc" id="L345">        log.info(&quot;renderView() for {}&quot;, siteNode);</span>

<span class="fc bfc" id="L347" title="All 2 branches covered.">        if (tagCloudMode)</span>
          {
<span class="fc" id="L349">            renderTagCloud();</span>
          }
        else
          {
<span class="fc" id="L353">            renderPosts(fullPosts, leadInPosts, linkedPosts);</span>
          }
<span class="fc" id="L355">      }</span>

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public Finder&lt;SiteNode&gt; findVirtualSiteNodes()
      {
<span class="fc" id="L365">        return new VirtualSiteNodeFinder(this);</span>
      }

    /*******************************************************************************************************************
     *
     * Renders the blog posts. Must be implemented by concrete subclasses.
     *
     * @param       fullPosts       the posts to be rendered in full
     * @param       leadinPosts     the posts to be rendered with lead in text
     * @param       linkedPosts     the posts to be rendered as references
     * @throws      Exception       if something fails
     *
     ******************************************************************************************************************/
    @SuppressWarnings(&quot;squid:S00112&quot;)
    protected abstract void renderPosts (@Nonnull List&lt;? extends Content&gt; fullPosts,
                                         @Nonnull List&lt;? extends Content&gt; leadinPosts,
                                         @Nonnull List&lt;? extends Content&gt; linkedPosts)
      throws Exception;

    /*******************************************************************************************************************
     *
     * Renders the tag cloud. Must be implemented by concrete subclasses.
     *
     * @param       tagsAndCount    the tags
     *
     ******************************************************************************************************************/
    @SuppressWarnings(&quot;squid:S00112&quot;)
    protected abstract void renderTagCloud (@Nonnull Collection&lt;? extends TagAndCount&gt; tagsAndCount);

    /*******************************************************************************************************************
     *
     * Creates a link for a {@link ResourcePath}.
     *
     * @param       path    the path
     * @return              the link
     *
     ******************************************************************************************************************/
    @Nonnull
    protected final String createLink (@Nonnull final ResourcePath path)
      {
<span class="fc" id="L405">        return siteNode.getSite().createLink(siteNode.getRelativeUri().appendedWith(path));</span>
      }

    /*******************************************************************************************************************
     *
     * Creates a link for a tag.
     *
     * @param       tag     the tag
     * @return              the link
     *
     ******************************************************************************************************************/
    @Nonnull
    protected final String createTagLink (final String tag)
      {
        // TODO: shouldn't ResourcePath always encode incoming strings?
shouldn't ResourcePath always encode incoming strings?
<span class="nc" id="L420"> var link = siteNode.getSite().createLink(siteNode.getRelativeUri().appendedWith(TAG_PREFIX)</span> <span class="nc" id="L421"> .appendedWith(encodedUtf8(tag)));</span> // TODO: Workaround because createLink() doesn't append trailing / if the link contains a dot. // Refactor by passing a parameter to createLink that overrides the default behaviour. <span class="nc bnc" id="L425" title="All 4 branches missed."> if (!link.endsWith(&quot;/&quot;) &amp;&amp; !link.contains(&quot;?&quot;))</span> { <span class="nc" id="L427"> link += &quot;/&quot;;</span> } <span class="nc" id="L430"> return link;</span> } /******************************************************************************************************************* * * ******************************************************************************************************************/ @Nonnull protected final ResourceProperties getViewProperties() { <span class="fc" id="L440"> return siteNode.getPropertyGroup(view.getId());</span> } /******************************************************************************************************************* * * Formats a date with the settings taken from the configuration and the request settings. * * @param dateTime the date to render * @return the formatted date * ******************************************************************************************************************/ @Nonnull protected final String formatDateTime (@Nonnull final ZonedDateTime dateTime) { <span class="fc" id="L454"> return dateTime.format(findDateTimeFormatter());</span> } /******************************************************************************************************************* * * Prepares the blog posts. * * @param context the rendering context * @param properties the view properties * @throws HttpStatusException status 404 if no post found * ******************************************************************************************************************/ protected final void prepareBlogPosts (@Nonnull final RenderContext context, @Nonnull final ResourceProperties properties) throws HttpStatusException { <span class="fc bfc" id="L469" title="All 2 branches covered."> final var maxFullItems = indexMode ? 0 : properties.getProperty(P_MAX_FULL_ITEMS).orElse(NO_LIMIT);</span> <span class="fc bfc" id="L470" title="All 2 branches covered."> final var maxLeadinItems = indexMode ? 0 : properties.getProperty(P_MAX_LEADIN_ITEMS).orElse(NO_LIMIT);</span> <span class="fc bfc" id="L471" title="All 2 branches covered."> final var maxItems = indexMode ? NO_LIMIT : properties.getProperty(P_MAX_ITEMS).orElse(NO_LIMIT);</span> <span class="fc" id="L473"> log.debug(&quot;&gt;&gt;&gt;&gt; preparing blog posts for {}: maxFullItems: {}, maxLeadinItems: {}, maxItems: {} (index: {}, tag: {}, uri: {})&quot;,</span> <span class="fc" id="L474"> view.getId(), maxFullItems, maxLeadinItems, maxItems, indexMode, tag.orElse(&quot;&quot;), uriOrCategory.orElse(&quot;&quot;));</span> <span class="fc" id="L476"> final var posts = findPosts(context, properties)</span> <span class="fc" id="L477"> .stream()</span> <span class="fc" id="L478"> .filter(post -&gt; post.getProperty(P_TITLE).isPresent())</span> <span class="fc" id="L479"> .sorted(REVERSE_DATE_COMPARATOR)</span> <span class="fc" id="L480"> .collect(toList());</span> <span class="fc bfc" id="L482" title="All 2 branches covered."> if (posts.isEmpty())</span> { <span class="fc" id="L484"> throw new HttpStatusException(SC_NOT_FOUND);</span> } <span class="fc" id="L487"> final var split = split(posts, 0, maxFullItems, maxFullItems + maxLeadinItems, maxItems);</span> <span class="fc" id="L488"> fullPosts.addAll(split.get(0));</span> <span class="fc" id="L489"> leadInPosts.addAll(split.get(1));</span> <span class="fc" id="L490"> linkedPosts.addAll(split.get(2));</span> <span class="fc" id="L491"> }</span> /******************************************************************************************************************* * * Renders the tag cloud. * ******************************************************************************************************************/ private void renderTagCloud() { <span class="fc" id="L500"> final var tagsAndCount = findAllPosts(getViewProperties())</span> <span class="fc" id="L501"> .stream()</span> <span class="fc" id="L502"> .flatMap(post -&gt; post.getProperty(P_TAGS).stream().flatMap(Collection::stream))</span> <span class="fc" id="L503"> .collect(toMap(t -&gt; t, TagAndCount::new, TagAndCount::reduced))</span> <span class="fc" id="L504"> .values()</span> <span class="fc" id="L505"> .stream()</span> <span class="fc" id="L506"> .sorted(comparing(TagAndCount::getTag))</span> <span class="fc" id="L507"> .collect(toList());</span> <span class="fc" id="L508"> renderTagCloud(withRanks(tagsAndCount));</span> <span class="fc" id="L509"> }</span> /******************************************************************************************************************* * * Finds all the relevant posts, applying filtering as needed. * ******************************************************************************************************************/ // TODO: use some short circuit to prevent from loading unnecessary data @Nonnull private List&lt;Content&gt; findPosts (@Nonnull final RenderContext context, @Nonnull final ResourceProperties properties) { <span class="fc" id="L520"> final var pathParams = context.getPathParams(siteNode);</span> <span class="fc bfc" id="L521" title="All 4 branches covered."> final var filtering = tag.isPresent() || uriOrCategory.isPresent();</span> <span class="fc" id="L522"> final var allPosts = findAllPosts(properties);</span> <span class="fc" id="L523"> final var posts = new ArrayList&lt;Content&gt;();</span> // // The thing works differently in function of pathParams: // when no pathParams, return all the posts; // when it matches a category, return all the posts in that category; // when it matches an exposed URI of a single specific post: // if not in 'index' mode, return only that post; // if in 'index' mode, returns all the posts. // <span class="fc bfc" id="L532" title="All 4 branches covered."> if (indexMode &amp;&amp; !filtering)</span> { <span class="fc" id="L534"> posts.addAll(allPosts);</span> } else { <span class="fc bfc" id="L538" title="All 2 branches covered."> if (tag.isPresent())</span> { <span class="fc" id="L540"> posts.addAll(filteredByTag(allPosts, tag.get()));</span> } else { <span class="fc" id="L544"> posts.addAll(filteredByExposedUri(allPosts, pathParams)</span> // pathParams matches an exposedUri; thus it's not a category, so an index wants all <span class="pc bpc" id="L546" title="1 of 2 branches missed."> .map(singlePost -&gt; indexMode ? allPosts : singletonList(singlePost))</span> // pathParams didn't match an exposedUri, so it's interpreted as a category to filter posts <span class="fc" id="L548"> .orElseGet(() -&gt; filteredByCategory(allPosts, uriOrCategory)));</span> } } <span class="fc" id="L552"> log.debug(&quot;&gt;&gt;&gt;&gt; found {} items&quot;, posts.size());</span> <span class="fc" id="L554"> return posts;</span> } /******************************************************************************************************************* * * Finds all the posts. * ******************************************************************************************************************/ @Nonnull private List&lt;Content&gt; findAllPosts (@Nonnull final ResourceProperties properties) { <span class="fc" id="L565"> return properties.getProperty(P_CONTENT_PATHS).orElse(emptyList()).stream()</span> <span class="fc" id="L566"> .flatMap(path -&gt; siteNode.getSite().find(_Content_).withRelativePath(path).stream()</span> <span class="fc" id="L567"> .flatMap(folder -&gt; folder.findChildren().stream()))</span> <span class="fc" id="L568"> .collect(toList());</span> } /******************************************************************************************************************* * * Returns the proper {@link DateTimeFormatter}. It is built from an explicit pattern, if defined in the current * {@link SiteNode}; otherwise the one provided by the {@link RequestLocaleManager} is used. The formatter is * configured with the time zone defined in the {@code SiteNode}, or a default is used. * * @return the {@code DateTimeFormatter} * ******************************************************************************************************************/ @Nonnull private DateTimeFormatter findDateTimeFormatter() { <span class="fc" id="L583"> final var locale = requestLocaleManager.getLocales().get(0);</span> <span class="fc" id="L584"> final var viewProperties = getViewProperties();</span> <span class="fc" id="L585"> final var dtf = viewProperties.getProperty(P_DATE_FORMAT)</span> <span class="fc" id="L586"> .map(s -&gt; s.replaceAll(&quot;EEEEE+&quot;, &quot;EEEE&quot;))</span> <span class="fc" id="L587"> .map(s -&gt; s.replaceAll(&quot;MMMMM+&quot;, &quot;MMMM&quot;))</span> <span class="fc bfc" id="L588" title="All 2 branches covered."> .map(p -&gt; (((p.length() == 2) ? DATETIME_FORMATTER_MAP_BY_STYLE.get(p).apply(locale)</span> <span class="fc" id="L589"> : DateTimeFormatter.ofPattern(p)).withLocale(locale)))</span> <span class="fc" id="L590"> .orElse(requestLocaleManager.getDateTimeFormatter());</span> <span class="fc" id="L592"> final var zoneId = viewProperties.getProperty(P_TIME_ZONE).orElse(DEFAULT_TIMEZONE);</span> <span class="fc" id="L593"> return dtf.withZone(ZoneId.of(zoneId));</span> } /******************************************************************************************************************* * * * ******************************************************************************************************************/ private void setDynamicProperties (@Nonnull final RenderContext context, @Nonnull final Content post) { <span class="fc" id="L603"> context.setDynamicNodeProperty(PD_TITLE, computeTitle(post));</span> <span class="fc" id="L604"> post.getExposedUri().map(this::createLink).ifPresent(l -&gt; context.setDynamicNodeProperty(PD_URL, l));</span> <span class="fc" id="L605"> post.getProperty(P_ID).ifPresent(id -&gt; context.setDynamicNodeProperty(PD_ID, id));</span> <span class="fc" id="L606"> post.getProperty(P_IMAGE_ID).ifPresent(id -&gt; context.setDynamicNodeProperty(PD_IMAGE_ID, id));</span> <span class="fc" id="L607"> }</span> /******************************************************************************************************************* * * ******************************************************************************************************************/ private void setTitle (@Nonnull final RenderContext context) { <span class="fc bfc" id="L615" title="All 2 branches covered."> if (tagCloudMode)</span> { <span class="fc" id="L617"> title = Optional.of(&quot;Tags&quot;);</span> } <span class="fc bfc" id="L619" title="All 2 branches covered."> else if (indexMode)</span> { <span class="fc" id="L621"> title = Optional.of(&quot;Post index&quot;);</span> <span class="fc bfc" id="L623" title="All 2 branches covered."> if (tag.isPresent())</span> { <span class="fc" id="L625"> title = Optional.of(String.format(&quot;Posts tagged as '%s'&quot;, tag.get()));</span> } <span class="fc" id="L627"> else uriOrCategory.ifPresent(s -&gt; title = Optional.of(String.format(&quot;Posts in category '%s'&quot;, s)));</span> } else { <span class="fc" id="L631"> title = getViewProperties().getProperty(P_TITLE).map(String::trim).flatMap(DefaultBlogViewController::filterEmptyString);</span> } <span class="fc" id="L634"> title.ifPresent(view::setTitle);</span> <span class="fc" id="L635"> title.ifPresent(s -&gt; context.setDynamicNodeProperty(PD_TITLE, s));</span> <span class="fc" id="L636"> }</span> /******************************************************************************************************************* * * ******************************************************************************************************************/ @Nonnull private String computeTitle (@Nonnull final Content post) { <span class="fc" id="L645"> final var prefix = siteNode.getProperty(P_TITLE).orElse(&quot;&quot;);</span> <span class="fc" id="L646"> final var title = post.getProperty(P_TITLE).orElse(&quot;&quot;);</span> <span class="pc bpc" id="L647" title="3 of 4 branches missed."> final var separator = &quot;&quot;.equals(prefix) || &quot;&quot;.equals(title) ? &quot;&quot; : &quot; - &quot;;</span> <span class="fc" id="L649"> return prefix + separator + title;</span> } /******************************************************************************************************************* * * ******************************************************************************************************************/ @Nonnull private static List&lt;TagAndCount&gt; withRanks (@Nonnull final Collection&lt;? extends TagAndCount&gt; tagsAndCount) { <span class="fc" id="L659"> final var counts = tagsAndCount.stream()</span> <span class="fc" id="L660"> .map(TagAndCount::getCount)</span> <span class="fc" id="L661"> .distinct()</span> <span class="fc" id="L662"> .sorted(reverseOrder())</span> <span class="fc" id="L663"> .collect(toList());</span> <span class="fc" id="L664"> return tagsAndCount.stream().map(tac -&gt; tac.withRank(rankOf(tac.count, counts))).collect(toList());</span> } /******************************************************************************************************************* * * Filters the given posts that match the selected category; returns all the posts if the category is empty. * * @param posts the source posts * @param category the category * @return the filtered posts * ******************************************************************************************************************/ @Nonnull private static List&lt;Content&gt; filteredByCategory (@Nonnull final List&lt;? extends Content&gt; posts, @Nonnull final Optional&lt;String&gt; category) { <span class="fc" id="L680"> return posts.stream().filter(post -&gt; hasCategory(post, category)).collect(toList());</span> } /******************************************************************************************************************* * * Filters the {@code sourcePosts} that matches the selected{@code tag}; returns all * posts if the category is empty. * * @param posts the source posts * @param tag the tag * @return the filtered posts * ******************************************************************************************************************/ @Nonnull private static List&lt;Content&gt; filteredByTag (@Nonnull final List&lt;? extends Content&gt; posts, @Nonnull final String tag) { <span class="fc" id="L696"> return posts.stream().filter(post -&gt; hasTag(post, tag)).collect(toList());</span> } /******************************************************************************************************************* * * ******************************************************************************************************************/ @Nonnull private static Optional&lt;Content&gt; filteredByExposedUri (@Nonnull final List&lt;Content&gt; posts, @Nonnull final ResourcePath exposedUri) { <span class="fc" id="L707"> return posts.stream().filter(post -&gt; post.getExposedUri().map(exposedUri::equals).orElse(false)).findFirst();</span> } /******************************************************************************************************************* * * ******************************************************************************************************************/ @Nonnull private static String rankOf (final int count, final List&lt;Integer&gt; counts) { <span class="pc bpc" id="L717" title="1 of 2 branches missed."> assert counts.contains(count);</span> <span class="fc" id="L718"> final var rank = counts.indexOf(count) + 1;</span> <span class="pc bpc" id="L719" title="1 of 2 branches missed."> return (rank &lt;= 10) ? Integer.toString(rank) : &quot;Others&quot;;</span> } /******************************************************************************************************************* * * ******************************************************************************************************************/ private static boolean hasCategory (@Nonnull final Content post, @Nonnull final Optional&lt;String&gt; category) { <span class="fc bfc" id="L728" title="All 4 branches covered."> return category.isEmpty() || post.getProperty(P_CATEGORY).equals(category);</span> } /******************************************************************************************************************* * * ******************************************************************************************************************/ private static boolean hasTag (@Nonnull final Content post, @Nonnull final String tag) { <span class="fc" id="L737"> return post.getProperty(P_TAGS).orElse(emptyList()).contains(tag);</span> } /******************************************************************************************************************* * * * ******************************************************************************************************************/ @Nonnull private static Optional&lt;String&gt; filterEmptyString (@Nonnull final String s) { <span class="nc bnc" id="L748" title="All 2 branches missed."> return &quot;&quot;.equals(s) ? Optional.empty() : Optional.of(s);</span> } } </pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.9.202303310957</span></div></body></html>