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 :: Basic</a> > <a href="../index.html" class="el_bundle">it-tidalwave-northernwind-core</a> > <a href="index.source.html" class="el_package">it.tidalwave.northernwind.core.model.spi</a> > <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 "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.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<RESPONSE_TYPE> implements ResponseBuilder<RESPONSE_TYPE></span>
{
// TODO: refactor with Key
protected static final String HEADER_CONTENT_LENGTH = "Content-Length";
protected static final String HEADER_ETAG = "ETag";
protected static final String HEADER_CONTENT_TYPE = "Content-Type";
protected static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
protected static final String HEADER_LAST_MODIFIED = "Last-Modified";
protected static final String HEADER_EXPIRES = "Expires";
protected static final String HEADER_LOCATION = "Location";
protected static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
protected static final String HEADER_IF_NONE_MATCH = "If-None-Match";
protected static final String HEADER_CACHE_CONTROL = "Cache-Control";
protected static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
<span class="fc" id="L77"> private static final String[] DATE_FORMATS =</span>
{
PATTERN_RFC1123,
"EEE, d MMM yyyy HH:mm:ss zzz",
"EEE, d-MMM-yy HH:mm:ss zzz",
"EEE MMM d HH:mm:ss yyyy"
};
/** 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<String> 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<ZonedDateTime> requestIfModifiedSince = Optional.empty();</span>
<span class="pc" id="L100"> @Getter @Setter @Nonnull</span>
private Supplier<Clock> clockSupplier = Clock::systemDefaultZone;
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public abstract ResponseBuilder<RESPONSE_TYPE> withHeader (@Nonnull String header, @Nonnull String value);
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> withHeaders (@Nonnull final Map<String, String> headers)
{
<span class="fc" id="L119"> ResponseBuilder<RESPONSE_TYPE> 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<RESPONSE_TYPE> withContentType (@Nonnull final String contentType)
{
<span class="fc" id="L137"> return withHeader(HEADER_CONTENT_TYPE, contentType);</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> withContentLength (final long contentLength)
{
<span class="fc" id="L148"> return withHeader(HEADER_CONTENT_LENGTH, "" + contentLength);</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> withContentDisposition (@Nonnull final String contentDisposition)
{
<span class="nc" id="L159"> return withHeader(HEADER_CONTENT_DISPOSITION, contentDisposition);</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> 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("max-age=%d", duration.getSeconds()));</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> 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("\"%d\"", time.toInstant().toEpochMilli()));</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> 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<RESPONSE_TYPE> 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<RESPONSE_TYPE> 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<RESPONSE_TYPE> forException (@Nonnull final NotFoundException e)
{
<span class="fc" id="L240"> log.info("NOT FOUND: {}", e.toString());</span>
<span class="fc" id="L241"> return forException(new HttpStatusException(SC_NOT_FOUND));</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> forException (@Nonnull final Throwable e)
{
<span class="fc" id="L252"> log.error("", e);</span>
<span class="fc" id="L253"> return forException(new HttpStatusException(SC_INTERNAL_SERVER_ERROR));</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> forException (@Nonnull final HttpStatusException e)
{
<span class="fc" id="L264"> var message = String.format("<h1>HTTP Status: %d</h1>%n", 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 = "<h1>Not found</h1>";</span>
<span class="fc" id="L273"> break;</span>
case SC_INTERNAL_SERVER_ERROR:
default: // FIXME: why?
<span class="fc" id="L277"> message = "<h1>Internal error</h1>";</span>
break;
}
<span class="fc" id="L281"> return withContentType("text/html")</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<RESPONSE_TYPE> 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<RESPONSE_TYPE> 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<RESPONSE_TYPE>)cacheSupport()).doBuild();</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override
public void put()
{
<span class="nc" id="L330"> ResponseHolder.THREAD_LOCAL.set(build());</span>
<span class="nc" 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<String> getHeader (@Nonnull String header);
/*******************************************************************************************************************
*
* Returns a header response previously added.
*
* @param header the header name
* @return the header value
*
******************************************************************************************************************/
@Nonnull
protected final Optional<ZonedDateTime> 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 "Not modified" response will be returned.
*
* @return itself for fluent interface style
*
******************************************************************************************************************/
@Nonnull
protected ResponseBuilder<RESPONSE_TYPE> 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(">>>> eTag: {} - requestIfNoneMatch: {}", eTag, requestIfNoneMatch);</span>
<span class="fc" id="L383"> log.debug(">>>> lastModified: {} - requestIfNotModifiedSince: {}", lastModified, requestIfModifiedSince);</span>
<span class="fc bfc" id="L385" title="All 4 branches covered."> if ( (eTag.isPresent() && eTag.equals(requestIfNoneMatch)) ||</span>
<span class="pc bpc" id="L386" title="1 of 4 branches missed."> (requestIfModifiedSince.isPresent() && lastModified.isPresent() &&</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<RESPONSE_TYPE> 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...
*
******************************************************************************************************************/
@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("Parsing {} with {}...", 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("{}", e.getMessage());</span>
}
}
<span class="nc" id="L431"> throw new IllegalArgumentException("Cannot parse date (see previous logs) " + 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("GMT"));</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>