DefaultCalendarViewController.java

  1. /*
  2.  * #%L
  3.  * *********************************************************************************************************************
  4.  *
  5.  * NorthernWind - lightweight CMS
  6.  * http://northernwind.tidalwave.it - git clone https://bitbucket.org/tidalwave/northernwind-src.git
  7.  * %%
  8.  * Copyright (C) 2011 - 2023 Tidalwave s.a.s. (http://tidalwave.it)
  9.  * %%
  10.  * *********************************************************************************************************************
  11.  *
  12.  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  13.  * the License. You may obtain a copy of the License at
  14.  *
  15.  *     http://www.apache.org/licenses/LICENSE-2.0
  16.  *
  17.  * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  18.  * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
  19.  * specific language governing permissions and limitations under the License.
  20.  *
  21.  * *********************************************************************************************************************
  22.  *
  23.  *
  24.  * *********************************************************************************************************************
  25.  * #L%
  26.  */
  27. package it.tidalwave.northernwind.frontend.ui.component.calendar;

  28. import javax.annotation.Nonnegative;
  29. import javax.annotation.Nonnull;
  30. import java.time.ZoneId;
  31. import java.time.ZonedDateTime;
  32. import java.util.List;
  33. import java.util.Map;
  34. import java.util.Optional;
  35. import java.util.SortedMap;
  36. import java.util.TreeMap;
  37. import java.util.stream.IntStream;
  38. import it.tidalwave.util.TimeProvider;
  39. import it.tidalwave.northernwind.core.model.HttpStatusException;
  40. import it.tidalwave.northernwind.core.model.RequestLocaleManager;
  41. import it.tidalwave.northernwind.core.model.ResourcePath;
  42. import it.tidalwave.northernwind.core.model.ResourceProperties;
  43. import it.tidalwave.northernwind.core.model.SiteNode;
  44. import it.tidalwave.northernwind.frontend.ui.RenderContext;
  45. import it.tidalwave.northernwind.frontend.ui.component.calendar.spi.CalendarDao;
  46. import lombok.RequiredArgsConstructor;
  47. import lombok.ToString;
  48. import lombok.extern.slf4j.Slf4j;
  49. import static java.util.Collections.emptyMap;
  50. import static java.util.stream.Collectors.*;
  51. import static javax.servlet.http.HttpServletResponse.*;
  52. import static it.tidalwave.northernwind.core.model.Content.P_TITLE;

  53. /***********************************************************************************************************************
  54.  *
  55.  * <p>A default implementation of the {@link CalendarViewController} that is independent of the presentation technology.
  56.  * This class is capable to render a yearly calendar with items and related links.</p>
  57.  *
  58.  * <p>It accepts a single path parameter {@code year} with selects a given year; otherwise the current year is used.</p>
  59.  *
  60.  * <p>Supported properties of the {@link SiteNode}:</p>
  61.  *
  62.  * <ul>
  63.  * <li>{@code P_ENTRIES}: a property with XML format that describes the entries;</li>
  64.  * <li>{@code P_SELECTED_YEAR}: the year to render (optional, otherwise the current year is used);</li>
  65.  * <li>{@code P_FIRST_YEAR}: the first available year;</li>
  66.  * <li>{@code P_LAST_YEAR}: the last available year ;</li>
  67.  * <li>{@code P_TITLE}: the page title (optional);</li>
  68.  * <li>{@code P_COLUMNS}: the number of columns of the table to render (optional, defaults to 4).</li>
  69.  * </ul>
  70.  *
  71.  * <p>The property {@code P_ENTRIES} must have the following structure:</p>
  72.  *
  73.  * <pre>
  74.  * &lt;?xml version="1.0" encoding="UTF-8"?&gt;
  75.  * &lt;calendar&gt;
  76.  *     &lt;year id="2004"&gt;
  77.  *         &lt;month id="jan"&gt;
  78.  *             &lt;item name="Provence" type="major" link="/diary/2004/01/02/"/&gt;
  79.  *             &lt;item name="Bocca di Magra" link="/diary/2004/01/24/"/&gt;
  80.  *             &lt;item name="Maremma" link="/diary/2004/01/31/"/&gt;
  81.  *         &lt;/month&gt;
  82.  *         ...
  83.  *     &lt;/year&gt;
  84.  *     ...
  85.  * &lt;/calendar&gt;
  86.  * </pre>
  87.  *
  88.  * <p>Concrete implementations must provide one method for rendering the calendar:</p>
  89.  *
  90.  * <ul>
  91.  * <li>{@link #render(int, int, int, java.util.Map)}</li>
  92.  * </ul>
  93.  *
  94.  * @author  Fabrizio Giudici
  95.  *
  96.  **********************************************************************************************************************/
  97. @RequiredArgsConstructor @Slf4j
  98. public abstract class DefaultCalendarViewController implements CalendarViewController
  99.   {
  100.     @RequiredArgsConstructor @ToString
  101.     public static class Entry
  102.       {
  103.         public final int month;
  104.         public final String name;
  105.         public final String link;
  106.         public final Optional<String> type;
  107.       }

  108.     @Nonnull
  109.     private final CalendarView view;

  110.     @Nonnull
  111.     private final SiteNode siteNode;

  112.     @Nonnull
  113.     protected final RequestLocaleManager requestLocaleManager;

  114.     @Nonnull
  115.     private final CalendarDao dao;

  116.     @Nonnull
  117.     private final TimeProvider timeProvider;

  118.     private int year;

  119.     private int firstYear;

  120.     private int lastYear;

  121.     private final SortedMap<Integer, List<Entry>> entriesByMonth = new TreeMap<>();

  122.     /*******************************************************************************************************************
  123.      *
  124.      * Compute stuff here, to eventually fail fast.
  125.      *
  126.      * {@inheritDoc}
  127.      *
  128.      ******************************************************************************************************************/
  129.     @Override
  130.     public void prepareRendering (@Nonnull final RenderContext context)
  131.       throws HttpStatusException
  132.       {
  133.         final var requestedYear = getRequestedYear(context.getPathParams(siteNode));
  134.         final var siteNodeProperties = siteNode.getProperties();
  135.         final var viewProperties = getViewProperties();

  136.         year      = viewProperties.getProperty(P_SELECTED_YEAR).orElse(requestedYear);
  137.         firstYear = viewProperties.getProperty(P_FIRST_YEAR).orElse(Math.min(year, requestedYear));
  138.         lastYear  = viewProperties.getProperty(P_LAST_YEAR).orElse(getCurrentYear());
  139.         log.info("prepareRendering() - {} f: {} l: {} r: {} y: {}", siteNode, firstYear, lastYear, requestedYear, year);

  140.         if ((year < firstYear) || (year > lastYear))
  141.           {
  142.             throw new HttpStatusException(SC_NOT_FOUND);
  143.           }

  144.         entriesByMonth.putAll(siteNodeProperties.getProperty(P_ENTRIES).map(e -> findEntriesForYear(e, year))
  145.                                                                        .orElse(emptyMap()));
  146.       }

  147.     /*******************************************************************************************************************
  148.      *
  149.      * {@inheritDoc}
  150.      *
  151.      ******************************************************************************************************************/
  152.     @Override
  153.     public void renderView (@Nonnull final RenderContext context)
  154.       {
  155.         render(siteNode.getProperty(P_TITLE), year, firstYear, lastYear, entriesByMonth, getViewProperties().getProperty(P_COLUMNS).orElse(4));
  156.       }

  157.     /*******************************************************************************************************************
  158.      *
  159.      * Renders the diary.
  160.      *
  161.      * @param       title       a title for the page (optional)
  162.      * @param       year        the current year
  163.      * @param       firstYear   the first available year
  164.      * @param       lastYear    the last available year
  165.      * @param       byMonth     a map of entries for the current year indexed by month
  166.      * @param       columns     the number of columns of the table to render
  167.      *
  168.      ******************************************************************************************************************/
  169.     protected abstract void render (@Nonnull final Optional<String> title,
  170.                                     @Nonnegative final int year,
  171.                                     @Nonnegative final int firstYear,
  172.                                     @Nonnegative final int lastYear,
  173.                                     @Nonnull final SortedMap<Integer, List<Entry>> byMonth,
  174.                                     final int columns);

  175.     /*******************************************************************************************************************
  176.      *
  177.      ******************************************************************************************************************/
  178.     @Nonnull
  179.     protected final ResourceProperties getViewProperties()
  180.       {
  181.         return siteNode.getPropertyGroup(view.getId());
  182.       }

  183.     /*******************************************************************************************************************
  184.      *
  185.      * Creates a link for the current year.
  186.      *
  187.      * @param       year        the year
  188.      * @return                  the link
  189.      *
  190.      ******************************************************************************************************************/
  191.     @Nonnull
  192.     protected final String createYearLink (final int year)
  193.       {
  194.         return siteNode.getSite().createLink(siteNode.getRelativeUri().appendedWith(Integer.toString(year)));
  195.       }

  196.     /*******************************************************************************************************************
  197.      *
  198.      * Retrieves a map of entries for the given year, indexed by month.
  199.      *
  200.      * @param       entries         the configuration data
  201.      * @param       year            the year
  202.      * @return                      the map
  203.      *
  204.      ******************************************************************************************************************/
  205.     @Nonnull
  206.     private Map<Integer, List<Entry>> findEntriesForYear (@Nonnull final String entries, @Nonnegative final int year)
  207.       {
  208.         return IntStream.rangeClosed(1, 12).boxed()
  209.                 .flatMap(month -> dao.findMonthlyEntries(siteNode.getSite(), entries, month, year).stream())
  210.                 .collect(groupingBy(e -> e.month));
  211.       }

  212.     /*******************************************************************************************************************
  213.      *
  214.      * Returns the current year reading it from the path params, or by default from the calendar.
  215.      *
  216.      ******************************************************************************************************************/
  217.     @Nonnegative
  218.     private int getRequestedYear (@Nonnull final ResourcePath pathParams)
  219.       throws HttpStatusException
  220.       {
  221.         if (pathParams.getSegmentCount() > 1)
  222.           {
  223.             throw new HttpStatusException(SC_BAD_REQUEST);
  224.           }

  225.         try
  226.           {
  227.             return pathParams.isEmpty() ? getCurrentYear() : Integer.parseInt(pathParams.getLeading());
  228.           }
  229.         catch (NumberFormatException e)
  230.           {
  231.             throw new HttpStatusException(SC_BAD_REQUEST);
  232.           }
  233.       }

  234.     /*******************************************************************************************************************
  235.      *
  236.      ******************************************************************************************************************/
  237.     @Nonnegative
  238.     private int getCurrentYear()
  239.       {
  240.         return ZonedDateTime.ofInstant(timeProvider.get(), ZoneId.of("UTC")).getYear();
  241.       }
  242.   }