Content of file ResponseBuilderSupport.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>ResponseBuilderSupport.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 :: Filesystems :: SCM :: Mercurial</a> &gt; <a href="../index.html" class="el_bundle">it-tidalwave-northernwind-core</a> &gt; <a href="index.source.html" class="el_package">it.tidalwave.northernwind.core.model.spi</a> &gt; <span class="el_source">ResponseBuilderSupport.java</span></div><h1>ResponseBuilderSupport.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.core.model.spi;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.NotThreadSafe;
import java.time.Clock;
import java.time.Duration;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import java.io.IOException;
import java.io.InputStream;
import it.tidalwave.util.NotFoundException;
import it.tidalwave.northernwind.core.model.HttpStatusException;
import it.tidalwave.northernwind.core.model.Request;
import it.tidalwave.northernwind.core.model.ResourceFile;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.*;

/***********************************************************************************************************************
 *
 * A partial implementation of {@link ResponseBuilder}.
 *
 * @author  Fabrizio Giudici
 *
 **********************************************************************************************************************/
<span class="fc" id="L60">@NotThreadSafe @Slf4j // FIXME: move to Core Default Implementation?</span>
<span class="fc" id="L61">public abstract class ResponseBuilderSupport&lt;RESPONSE_TYPE&gt; implements ResponseBuilder&lt;RESPONSE_TYPE&gt;</span>
  {
    // TODO: refactor with Key
    protected static final String HEADER_CONTENT_LENGTH = &quot;Content-Length&quot;;
    protected static final String HEADER_ETAG = &quot;ETag&quot;;
    protected static final String HEADER_CONTENT_TYPE = &quot;Content-Type&quot;;
    protected static final String HEADER_CONTENT_DISPOSITION = &quot;Content-Disposition&quot;;
    protected static final String HEADER_LAST_MODIFIED = &quot;Last-Modified&quot;;
    protected static final String HEADER_EXPIRES = &quot;Expires&quot;;
    protected static final String HEADER_LOCATION = &quot;Location&quot;;
    protected static final String HEADER_IF_MODIFIED_SINCE = &quot;If-Modified-Since&quot;;
    protected static final String HEADER_IF_NONE_MATCH = &quot;If-None-Match&quot;;
    protected static final String HEADER_CACHE_CONTROL = &quot;Cache-Control&quot;;

    protected static final String PATTERN_RFC1123 = &quot;EEE, dd MMM yyyy HH:mm:ss zzz&quot;;

<span class="fc" id="L77">    private static final String[] DATE_FORMATS =</span>
      {
        PATTERN_RFC1123,
        &quot;EEE, d MMM yyyy HH:mm:ss zzz&quot;,
        &quot;EEE, d-MMM-yy HH:mm:ss zzz&quot;,
        &quot;EEE MMM d HH:mm:ss yyyy&quot;
      };

    /** The body of the response. */
<span class="fc" id="L86">    @Nonnull</span>
    protected Object body = new byte[0];

    /** The HTTP status of the response. */
<span class="fc" id="L90">    protected int httpStatus = SC_OK;</span>

    /** The If-None-Match header specified in the request we're responding to. */
<span class="fc" id="L93">    @Nonnull</span>
<span class="fc" id="L94">    protected Optional&lt;String&gt; requestIfNoneMatch = Optional.empty();</span>

    /** The If-Modified-Since header specified in the request we're responding to. */
<span class="fc" id="L97">    @Nonnull</span>
<span class="fc" id="L98">    protected Optional&lt;ZonedDateTime&gt; requestIfModifiedSince = Optional.empty();</span>

<span class="pc" id="L100">    @Getter @Setter @Nonnull</span>
    private Supplier&lt;Clock&gt; clockSupplier = Clock::systemDefaultZone;

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public abstract ResponseBuilder&lt;RESPONSE_TYPE&gt; withHeader (@Nonnull String header, @Nonnull String value);

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public ResponseBuilder&lt;RESPONSE_TYPE&gt; withHeaders (@Nonnull final Map&lt;String, String&gt; headers)
      {
<span class="fc" id="L119">        ResponseBuilder&lt;RESPONSE_TYPE&gt; result = this;</span>

<span class="pc bpc" id="L121" title="1 of 2 branches missed.">        for (final var entry : headers.entrySet())</span>
          {
<span class="nc" id="L123">            result = result.withHeader(entry.getKey(), entry.getValue());</span>
<span class="nc" id="L124">          }</span>

<span class="fc" id="L126">        return result;</span>
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public ResponseBuilder&lt;RESPONSE_TYPE&gt; withContentType (@Nonnull final String contentType)
      {
<span class="fc" id="L137">        return withHeader(HEADER_CONTENT_TYPE, contentType);</span>
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public ResponseBuilder&lt;RESPONSE_TYPE&gt; withContentLength (final long contentLength)
      {
<span class="fc" id="L148">        return withHeader(HEADER_CONTENT_LENGTH, &quot;&quot; + contentLength);</span>
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public ResponseBuilder&lt;RESPONSE_TYPE&gt; withContentDisposition (@Nonnull final String contentDisposition)
      {
<span class="nc" id="L159">        return withHeader(HEADER_CONTENT_DISPOSITION, contentDisposition);</span>
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public ResponseBuilder&lt;RESPONSE_TYPE&gt; withExpirationTime (@Nonnull final Duration duration)
      {
<span class="fc" id="L170">        final var expirationTime = ZonedDateTime.now(clockSupplier.get()).plus(duration);</span>
<span class="fc" id="L171">        return withHeader(HEADER_EXPIRES, createFormatter(PATTERN_RFC1123).format(expirationTime))</span>
<span class="fc" id="L172">              .withHeader(HEADER_CACHE_CONTROL, String.format(&quot;max-age=%d&quot;, duration.getSeconds()));</span>
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public ResponseBuilder&lt;RESPONSE_TYPE&gt; withLatestModifiedTime (@Nonnull final ZonedDateTime time)
      {
<span class="fc" id="L183">        return withHeader(HEADER_LAST_MODIFIED, createFormatter(PATTERN_RFC1123).format(time))</span>
<span class="fc" id="L184">              .withHeader(HEADER_ETAG, String.format(&quot;\&quot;%d\&quot;&quot;, time.toInstant().toEpochMilli()));</span>
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public ResponseBuilder&lt;RESPONSE_TYPE&gt; withBody (@Nonnull final Object body)
      {
<span class="fc bfc" id="L195" title="All 2 branches covered.">        this.body = (body instanceof byte[]) ? body :</span>
<span class="pc bpc" id="L196" title="1 of 2 branches missed.">                    (body instanceof InputStream) ? body :</span>
<span class="fc" id="L197">                     body.toString().getBytes(UTF_8);</span>
<span class="fc" id="L198">        return this;</span>
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public ResponseBuilder&lt;RESPONSE_TYPE&gt; fromFile (@Nonnull final ResourceFile file)
      throws IOException
      {
<span class="fc" id="L210">        final var bytes = file.asBytes(); // TODO: this always loads, in some cases would not be needed</span>

<span class="fc" id="L212">        return withContentType(file.getMimeType())</span>
<span class="fc" id="L213">              .withContentLength(bytes.length)</span>
<span class="fc" id="L214">              .withLatestModifiedTime(file.getLatestModificationTime())</span>
<span class="fc" id="L215">              .withBody(bytes);</span>
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public ResponseBuilder&lt;RESPONSE_TYPE&gt; forRequest (@Nonnull final Request request)
      {
<span class="fc" id="L226">        this.requestIfNoneMatch = request.getHeader(HEADER_IF_NONE_MATCH);</span>
<span class="fc" id="L227">        this.requestIfModifiedSince = request.getHeader(HEADER_IF_MODIFIED_SINCE).map(ResponseBuilderSupport::parseDate);</span>

<span class="fc" id="L229">        return this;</span>
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public ResponseBuilder&lt;RESPONSE_TYPE&gt; forException (@Nonnull final NotFoundException e)
      {
<span class="fc" id="L240">        log.info(&quot;NOT FOUND: {}&quot;, e.toString());</span>
<span class="fc" id="L241">        return forException(new HttpStatusException(SC_NOT_FOUND));</span>
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public ResponseBuilder&lt;RESPONSE_TYPE&gt; forException (@Nonnull final Throwable e)
      {
<span class="fc" id="L252">        log.error(&quot;&quot;, e);</span>
<span class="fc" id="L253">        return forException(new HttpStatusException(SC_INTERNAL_SERVER_ERROR));</span>
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public ResponseBuilder&lt;RESPONSE_TYPE&gt; forException (@Nonnull final HttpStatusException e)
      {
<span class="fc" id="L264">        var message = String.format(&quot;&lt;h1&gt;HTTP Status: %d&lt;/h1&gt;%n&quot;, e.getHttpStatus());</span>

<span class="pc bpc" id="L266" title="1 of 3 branches missed.">        switch (e.getHttpStatus()) // FIXME: get from a resource bundle</span>
          {
            case SC_MOVED_TEMPORARILY:
<span class="nc" id="L269">              break;</span>

            case SC_NOT_FOUND:
<span class="fc" id="L272">              message = &quot;&lt;h1&gt;Not found&lt;/h1&gt;&quot;;</span>
<span class="fc" id="L273">              break;</span>

            case SC_INTERNAL_SERVER_ERROR:
            default: // FIXME: why?
<span class="fc" id="L277">              message = &quot;&lt;h1&gt;Internal error&lt;/h1&gt;&quot;;</span>
              break;
          }

<span class="fc" id="L281">        return withContentType(&quot;text/html&quot;)</span>
<span class="fc" id="L282">              .withHeaders(e.getHeaders())</span>
<span class="fc" id="L283">              .withBody(message)</span>
<span class="fc" id="L284">              .withStatus(e.getHttpStatus());</span>
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public ResponseBuilder&lt;RESPONSE_TYPE&gt; withStatus (final int httpStatus)
      {
<span class="fc" id="L295">        this.httpStatus = httpStatus;</span>
<span class="fc" id="L296">        return this;</span>
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public ResponseBuilder&lt;RESPONSE_TYPE&gt; permanentRedirect (@Nonnull final String url)
      {
<span class="fc" id="L307">        return withHeader(HEADER_LOCATION, url)</span>
<span class="fc" id="L308">              .withStatus(SC_MOVED_PERMANENTLY);</span>
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override @Nonnull
    public final RESPONSE_TYPE build()
      {
<span class="fc" id="L319">        return ((ResponseBuilderSupport&lt;RESPONSE_TYPE&gt;)cacheSupport()).doBuild();</span>
      }

    /*******************************************************************************************************************
     *
     * {@inheritDoc}
     *
     ******************************************************************************************************************/
    @Override
    public void put()
      {
<span class="fc" id="L330">        ResponseHolder.THREAD_LOCAL.set(build());</span>
<span class="fc" id="L331">      }</span>

    /*******************************************************************************************************************
     *
     * This method actually builds the response and must be provided by concrete subclasses.
     *
     * @return  the response
     *
     ******************************************************************************************************************/
    @Nonnull
    protected abstract RESPONSE_TYPE doBuild();

    /*******************************************************************************************************************
     *
     * Returns a header response previously added.
     *
     * @param   header  the header name
     * @return          the header value
     *
     ******************************************************************************************************************/
    @Nonnull
    protected abstract Optional&lt;String&gt; getHeader (@Nonnull String header);

    /*******************************************************************************************************************
     *
     * Returns a header response previously added.
     *
     * @param   header  the header name
     * @return          the header value
     *
     ******************************************************************************************************************/
    @Nonnull
    protected final Optional&lt;ZonedDateTime&gt; getDateTimeHeader (@Nonnull final String header)
      {
<span class="fc" id="L365">        return getHeader(header).map(ResponseBuilderSupport::parseDate);</span>
      }

    /*******************************************************************************************************************
     *
     * Takes care of the caching feature. If the response refers to an entity whose value has been cached by the
     * client and it's still fresh, a &quot;Not modified&quot; response will be returned.
     *
     * @return                      itself for fluent interface style
     *
     ******************************************************************************************************************/
    @Nonnull
    protected ResponseBuilder&lt;RESPONSE_TYPE&gt; cacheSupport()
      {
<span class="fc" id="L379">        final var eTag = getHeader(HEADER_ETAG);</span>
<span class="fc" id="L380">        final var lastModified = getDateTimeHeader(HEADER_LAST_MODIFIED);</span>

<span class="fc" id="L382">        log.debug(&quot;&gt;&gt;&gt;&gt; eTag: {} - requestIfNoneMatch: {}&quot;, eTag, requestIfNoneMatch);</span>
<span class="fc" id="L383">        log.debug(&quot;&gt;&gt;&gt;&gt; lastModified: {} - requestIfNotModifiedSince: {}&quot;, lastModified, requestIfModifiedSince);</span>

<span class="fc bfc" id="L385" title="All 4 branches covered.">        if ( (eTag.isPresent() &amp;&amp; eTag.equals(requestIfNoneMatch)) ||</span>
<span class="pc bpc" id="L386" title="1 of 4 branches missed.">             (requestIfModifiedSince.isPresent() &amp;&amp; lastModified.isPresent() &amp;&amp;</span>
<span class="fc bfc" id="L387" title="All 4 branches covered.">              (lastModified.get().isBefore(requestIfModifiedSince.get()) || lastModified.get().isEqual(requestIfModifiedSince.get()))) )</span>
          {
<span class="fc" id="L389">            return notModified();</span>
          }

<span class="fc" id="L392">        return this;</span>
      }

    /*******************************************************************************************************************
     *
     *
     *
     ******************************************************************************************************************/
    @Nonnull
    private ResponseBuilder&lt;RESPONSE_TYPE&gt; notModified()
      {
<span class="fc" id="L403">        return withBody(new byte[0])</span>
<span class="fc" id="L404">              .withContentLength(0)</span>
<span class="fc" id="L405">              .withStatus(SC_NOT_MODIFIED);</span>
      }

    /*******************************************************************************************************************
     *
     * Parse a date with one of the valid formats for HTTP headers.
     *
     * FIXME: we should try to avoid depending on this stuff...
we should try to avoid depending on this stuff...
* ******************************************************************************************************************/ @Nonnull private static ZonedDateTime parseDate (@Nonnull final String string) { <span class="pc bpc" id="L418" title="1 of 2 branches missed."> for (final var dateFormat : DATE_FORMATS)</span> { try { <span class="fc" id="L422"> log.debug(&quot;Parsing {} with {}...&quot;, string, dateFormat);</span> <span class="fc" id="L423"> return ZonedDateTime.parse(string, createFormatter(dateFormat));</span> } <span class="fc" id="L425"> catch (DateTimeParseException e)</span> { <span class="fc" id="L427"> log.debug(&quot;{}&quot;, e.getMessage());</span> } } <span class="nc" id="L431"> throw new IllegalArgumentException(&quot;Cannot parse date (see previous logs) &quot; + string);</span> } /******************************************************************************************************************* * * * ******************************************************************************************************************/ @Nonnull /* package */ static DateTimeFormatter createFormatter (@Nonnull final String template) { <span class="fc" id="L442"> return DateTimeFormatter.ofPattern(template, Locale.US).withZone(ZoneId.of(&quot;GMT&quot;));</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>