<?xml version="1.0" encoding="UTF-8"?><!DOCTYPEhtmlPUBLIC"-//W3C//DTD XHTML 1.0 Strict//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><htmlxmlns="http://www.w3.org/1999/xhtml"lang="en"><head><metahttp-equiv="Content-Type"content="text/html;charset=UTF-8"/><linkrel="stylesheet"href="../../jacoco-resources/report.css"type="text/css"/><linkrel="shortcut icon"href="../../jacoco-resources/report.gif"type="image/gif"/><title>ResponseBuilderSupport.java</title><linkrel="stylesheet"href="../../jacoco-resources/prettify.css"type="text/css"/><scripttype="text/javascript"src="../../jacoco-resources/prettify.js"></script></head><bodyonload="window['PR_TAB_WIDTH']=4;prettyPrint()"><divclass="breadcrumb"id="breadcrumb"><spanclass="info"><ahref="../../jacoco-sessions.html"class="el_session">Sessions</a></span><ahref="../../index.html"class="el_report">NorthernWind :: Filesystems :: SCM :: Mercurial</a>><ahref="../index.html"class="el_bundle">it-tidalwave-northernwind-core</a>><ahref="index.source.html"class="el_package">it.tidalwave.northernwind.core.model.spi</a>><spanclass="el_source">ResponseBuilderSupport.java</span></div><h1>ResponseBuilderSupport.java</h1><preclass="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
*
**********************************************************************************************************************/
<spanclass="fc"id="L60">@NotThreadSafe @Slf4j // FIXME: move to Core Default Implementation?</span>
move to Core Default Implementation?
<spanclass="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";
<spanclass="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. */
<spanclass="fc"id="L86"> @Nonnull</span>
protected Object body = new byte[0];
/** The HTTP status of the response. */
<spanclass="fc"id="L90"> protected int httpStatus = SC_OK;</span>
/** The If-None-Match header specified in the request we're responding to. */
<spanclass="fc"id="L93"> @Nonnull</span><spanclass="fc"id="L94"> protected Optional<String> requestIfNoneMatch = Optional.empty();</span>
/** The If-Modified-Since header specified in the request we're responding to. */
<spanclass="fc"id="L97"> @Nonnull</span><spanclass="fc"id="L98"> protected Optional<ZonedDateTime> requestIfModifiedSince = Optional.empty();</span><spanclass="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)
{
<spanclass="fc"id="L119"> ResponseBuilder<RESPONSE_TYPE> result = this;</span><spanclass="pc bpc"id="L121"title="1 of 2 branches missed."> for (final var entry : headers.entrySet())</span>
{
<spanclass="nc"id="L123"> result = result.withHeader(entry.getKey(), entry.getValue());</span><spanclass="nc"id="L124"> }</span><spanclass="fc"id="L126"> return result;</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> withContentType (@Nonnull final String contentType)
{
<spanclass="fc"id="L137"> return withHeader(HEADER_CONTENT_TYPE, contentType);</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> withContentLength (final long contentLength)
{
<spanclass="fc"id="L148"> return withHeader(HEADER_CONTENT_LENGTH, "" + contentLength);</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> withContentDisposition (@Nonnull final String contentDisposition)
{
<spanclass="nc"id="L159"> return withHeader(HEADER_CONTENT_DISPOSITION, contentDisposition);</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> withExpirationTime (@Nonnull final Duration duration)
{
<spanclass="fc"id="L170"> final var expirationTime = ZonedDateTime.now(clockSupplier.get()).plus(duration);</span><spanclass="fc"id="L171"> return withHeader(HEADER_EXPIRES, createFormatter(PATTERN_RFC1123).format(expirationTime))</span><spanclass="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)
{
<spanclass="fc"id="L183"> return withHeader(HEADER_LAST_MODIFIED, createFormatter(PATTERN_RFC1123).format(time))</span><spanclass="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)
{
<spanclass="fc bfc"id="L195"title="All 2 branches covered."> this.body = (body instanceof byte[]) ? body :</span><spanclass="pc bpc"id="L196"title="1 of 2 branches missed."> (body instanceof InputStream) ? body :</span><spanclass="fc"id="L197"> body.toString().getBytes(UTF_8);</span><spanclass="fc"id="L198"> return this;</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> fromFile (@Nonnull final ResourceFile file)
throws IOException
{
<spanclass="fc"id="L210"> final var bytes = file.asBytes(); // TODO: this always loads, in some cases would not be needed</span><spanclass="fc"id="L212"> return withContentType(file.getMimeType())</span><spanclass="fc"id="L213"> .withContentLength(bytes.length)</span><spanclass="fc"id="L214"> .withLatestModifiedTime(file.getLatestModificationTime())</span><spanclass="fc"id="L215"> .withBody(bytes);</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> forRequest (@Nonnull final Request request)
{
<spanclass="fc"id="L226"> this.requestIfNoneMatch = request.getHeader(HEADER_IF_NONE_MATCH);</span><spanclass="fc"id="L227"> this.requestIfModifiedSince = request.getHeader(HEADER_IF_MODIFIED_SINCE).map(ResponseBuilderSupport::parseDate);</span><spanclass="fc"id="L229"> return this;</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> forException (@Nonnull final NotFoundException e)
{
<spanclass="fc"id="L240"> log.info("NOT FOUND: {}", e.toString());</span><spanclass="fc"id="L241"> return forException(new HttpStatusException(SC_NOT_FOUND));</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> forException (@Nonnull final Throwable e)
{
<spanclass="fc"id="L252"> log.error("", e);</span><spanclass="fc"id="L253"> return forException(new HttpStatusException(SC_INTERNAL_SERVER_ERROR));</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> forException (@Nonnull final HttpStatusException e)
{
<spanclass="fc"id="L264"> var message = String.format("<h1>HTTP Status: %d</h1>%n", e.getHttpStatus());</span><spanclass="pc bpc"id="L266"title="1 of 3 branches missed."> switch (e.getHttpStatus()) // FIXME: get from a resource bundle</span>
{
case SC_MOVED_TEMPORARILY:
<spanclass="nc"id="L269"> break;</span>
case SC_NOT_FOUND:
<spanclass="fc"id="L272"> message = "<h1>Not found</h1>";</span><spanclass="fc"id="L273"> break;</span>
case SC_INTERNAL_SERVER_ERROR:
default: // FIXME: why?
<spanclass="fc"id="L277"> message = "<h1>Internal error</h1>";</span>
break;
}
<spanclass="fc"id="L281"> return withContentType("text/html")</span><spanclass="fc"id="L282"> .withHeaders(e.getHeaders())</span><spanclass="fc"id="L283"> .withBody(message)</span><spanclass="fc"id="L284"> .withStatus(e.getHttpStatus());</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> withStatus (final int httpStatus)
{
<spanclass="fc"id="L295"> this.httpStatus = httpStatus;</span><spanclass="fc"id="L296"> return this;</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public ResponseBuilder<RESPONSE_TYPE> permanentRedirect (@Nonnull final String url)
{
<spanclass="fc"id="L307"> return withHeader(HEADER_LOCATION, url)</span><spanclass="fc"id="L308"> .withStatus(SC_MOVED_PERMANENTLY);</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override @Nonnull
public final RESPONSE_TYPE build()
{
<spanclass="fc"id="L319"> return ((ResponseBuilderSupport<RESPONSE_TYPE>)cacheSupport()).doBuild();</span>
}
/*******************************************************************************************************************
*
* {@inheritDoc}
*
******************************************************************************************************************/
@Override
public void put()
{
<spanclass="fc"id="L330"> ResponseHolder.THREAD_LOCAL.set(build());</span><spanclass="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<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)
{
<spanclass="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()
{
<spanclass="fc"id="L379"> final var eTag = getHeader(HEADER_ETAG);</span><spanclass="fc"id="L380"> final var lastModified = getDateTimeHeader(HEADER_LAST_MODIFIED);</span><spanclass="fc"id="L382"> log.debug(">>>> eTag: {} - requestIfNoneMatch: {}", eTag, requestIfNoneMatch);</span><spanclass="fc"id="L383"> log.debug(">>>> lastModified: {} - requestIfNotModifiedSince: {}", lastModified, requestIfModifiedSince);</span><spanclass="fc bfc"id="L385"title="All 4 branches covered."> if ( (eTag.isPresent() && eTag.equals(requestIfNoneMatch)) ||</span><spanclass="pc bpc"id="L386"title="1 of 4 branches missed."> (requestIfModifiedSince.isPresent() && lastModified.isPresent() &&</span><spanclass="fc bfc"id="L387"title="All 4 branches covered."> (lastModified.get().isBefore(requestIfModifiedSince.get()) || lastModified.get().isEqual(requestIfModifiedSince.get()))) )</span>
{
<spanclass="fc"id="L389"> return notModified();</span>
}
<spanclass="fc"id="L392"> return this;</span>
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private ResponseBuilder<RESPONSE_TYPE> notModified()
{
<spanclass="fc"id="L403"> return withBody(new byte[0])</span><spanclass="fc"id="L404"> .withContentLength(0)</span><spanclass="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)
{
<spanclass="pc bpc"id="L418"title="1 of 2 branches missed."> for (final var dateFormat : DATE_FORMATS)</span>
{
try
{
<spanclass="fc"id="L422"> log.debug("Parsing {} with {}...", string, dateFormat);</span><spanclass="fc"id="L423"> return ZonedDateTime.parse(string, createFormatter(dateFormat));</span>
}
<spanclass="fc"id="L425"> catch (DateTimeParseException e)</span>
{
<spanclass="fc"id="L427"> log.debug("{}", e.getMessage());</span>
}
}
<spanclass="nc"id="L431"> throw new IllegalArgumentException("Cannot parse date (see previous logs) " + string);</span>
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
/* package */ static DateTimeFormatter createFormatter (@Nonnull final String template)
{
<spanclass="fc"id="L442"> return DateTimeFormatter.ofPattern(template, Locale.US).withZone(ZoneId.of("GMT"));</span>
}
}
</pre><divclass="footer"><spanclass="right">Created with <ahref="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.9.202303310957</span></div></body></html>