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 :: HTML Template</a> > <a href="../index.html" class="el_bundle">it-tidalwave-northernwind-frontend-components</a> > <a href="index.source.html" class="el_package">it.tidalwave.northernwind.frontend.ui.component.blog</a> > <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 "License"); 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 "AS IS" 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;
/***********************************************************************************************************************
*
* <p>A default implementation of the {@link BlogViewController} that is independent of the presentation technology.
* This class is capable to render:</p>
*
* <ul>
* <li>blog posts (in various ways)</li>
* <li>an index of the blog</li>
* <li>a tag cloud</li>
* </ul>
*
* <p>It accepts path parameters as follows:</p>
*
* <ul>
* <li>{@code <uri>}: selects a single post with the given uri;</li>
* <li>{@code <category>}: selects posts with the given category;</li>
* <li>{@code tags/<tag>}: selects posts with the given tag;</li>
* <li>{@code index}: renders a post index, with links to single posts;</li>
* <li>{@code index/<category>}: renders an index of posts with the given category;</li>
* <li>{@code index/tag/<tag>}: renders an index of posts with the given tag.</li>
* </ul>
*
* <p>Supported properties of the {@link SiteNode}:</p>
*
* <ul>
* <li>{@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;</li>
* <li>{@code P_MAX_FULL_ITEMS}: the max. number of posts to be rendered in full;</li>
* <li>{@code P_MAX_LEADIN_ITEMS}: the max. number of posts to be rendered with lead-in text;</li>
* <li>{@code P_MAX_ITEMS}: the max. number of posts to be rendered as links;</li>
* <li>{@code P_DATE_FORMAT}: the pattern for formatting date and times;</li>
* <li>{@code P_TIME_ZONE}: the time zone for rendering dates (defaults to CET);</li>
* <li>{@code P_INDEX}: if {@code true}, forces an index rendering (useful e.g. when used in sidebars);</li>
* <li>{@code P_TAG_CLOUD}: if {@code true}, forces a tag cloud rendering (useful e.g. when used in sidebars).</li>
* </ul>
*
* <p>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.</p>
*
* <p>Supported properties of the {@link Content}:</p>
*
* <ul>
* <li>{@code P_TITLE}: the title;</li>
* <li>{@code P_FULL_TEXT}: the full text;</li>
* <li>{@code P_LEADIN_TEXT}: the lead-in text;</li>
* <li>{@code P_ID}: the unique id;</li>
* <li>{@code P_IMAGE_ID}: the id of an image representative of the post;</li>
* <li>{@code P_PUBLISHING_DATE}: the publishing date;</li>
* <li>{@code P_CREATION_DATE}: the creation date;</li>
* <li>{@code P_TAGS}: the tags;</li>
* <li>{@code P_CATEGORY}: the category.</li>
* </ul>
*
* <p>When preparing for rendering, the following dynamic properties will be set, only if a single post is rendered:</p>
*
* <ul>
* <li>{@code PD_URL}: the canonical URL of the post;</li>
* <li>{@code PD_ID}: the unique id of the post;</li>
* <li>{@code PD_IMAGE_ID}: the id of the representative image.</li>
* </ul>
*
* <p>Concrete implementations must provide two methods for rendering the blog posts and the tag cloud:</p>
*
* <ul>
* <li>{@link #renderPosts(java.util.List, java.util.List, java.util.List) }</li>
* <li>{@link #renderTagCloud(java.util.Collection) }</li>
* </ul>
*
* @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, "");</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("Mismatching " + this + " vs " + other);</span>
}
<span class="fc" id="L174"> return new TagAndCount(tag, this.count + other.count, "");</span>
}
@Override @Nonnull
public String toString()
{
<span class="fc" id="L180"> return String.format("TagAndCount(%s, %d, %s)", 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<SiteNode, VirtualSiteNodeFinder>
{
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<SiteNode> 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 -> log.trace(">>>> virtual node for: {}", p.getExposedUri()))</span>
<span class="fc" id="L212"> .flatMap(post -> createVirtualNode(post).stream())</span>
<span class="fc" id="L213"> .collect(toList());</span>
}
@Nonnull
private Optional<VirtualSiteNode> 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 -> 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<String, Function<Locale, DateTimeFormatter>> DATETIME_FORMATTER_MAP_BY_STYLE = new HashMap<>();</span>
static
{
<span class="fc" id="L230"> DATETIME_FORMATTER_MAP_BY_STYLE.put("S-", locale -> getDateTimeFormatterFor(FormatStyle.SHORT, locale));</span>
<span class="fc" id="L231"> DATETIME_FORMATTER_MAP_BY_STYLE.put("M-", locale -> getDateTimeFormatterFor(FormatStyle.MEDIUM, locale));</span>
<span class="fc" id="L232"> DATETIME_FORMATTER_MAP_BY_STYLE.put("L-", locale -> getDateTimeFormatterFor(FormatStyle.LONG, locale));</span>
<span class="fc" id="L233"> DATETIME_FORMATTER_MAP_BY_STYLE.put("F-", locale -> getDateTimeFormatterFor(FormatStyle.FULL, locale));</span>
}
<span class="fc" id="L236"> protected static final List<Key<ZonedDateTime>> 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("GMT"));</span>
public static final String DEFAULT_TIMEZONE = "CET";
private static final int NO_LIMIT = 9999;
private static final String INDEX_PREFIX = "index";
private static final String TAG_PREFIX = "tag";
<span class="fc" id="L248"> private static final ResourcePath TAG_CLOUD = ResourcePath.of("tags");</span>
<span class="fc" id="L250"> private static final Comparator<Content> REVERSE_DATE_COMPARATOR = (p1, p2) -></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<String> tag = Optional.empty();</span>
<span class="fc" id="L264"> private Optional<String> uriOrCategory = Optional.empty();</span>
private boolean indexMode;
private boolean tagCloudMode;
<span class="fc" id="L270"> protected Optional<String> title = Optional.empty();</span>
<span class="fc" id="L272"> /* VisibleForTesting */ final List<Content> fullPosts = new ArrayList<>();</span>
<span class="fc" id="L274"> /* VisibleForTesting */ final List<Content> leadInPosts = new ArrayList<>();</span>
<span class="fc" id="L276"> /* VisibleForTesting */ final List<Content> linkedPosts = new ArrayList<>();</span>
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override
public void prepareRendering (@Nonnull final RenderContext context)
throws HttpStatusException
{
<span class="fc" id="L287"> log.info("prepareRendering(RenderContext) for {}", 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) && (pathParams.getSegmentCount() == 2)) // matches(TAG_PREFIX, ".*")</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) && leadInPosts.isEmpty() && 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("renderView() for {}", 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<SiteNode> 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("squid:S00112")
protected abstract void renderPosts (@Nonnull List<? extends Content> fullPosts,
@Nonnull List<? extends Content> leadinPosts,
@Nonnull List<? extends Content> linkedPosts)
throws Exception;
/*******************************************************************************************************************
*
* Renders the tag cloud. Must be implemented by concrete subclasses.
*
* @param tagsAndCount the tags
*
******************************************************************************************************************/
@SuppressWarnings("squid:S00112")
protected abstract void renderTagCloud (@Nonnull Collection<? extends TagAndCount> 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?