CachingRestClientSupport.java

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

  28. import javax.annotation.Nonnegative;
  29. import javax.annotation.Nonnull;
  30. import javax.annotation.PostConstruct;
  31. import java.util.List;
  32. import java.util.Optional;
  33. import java.io.IOException;
  34. import java.nio.file.Path;
  35. import java.net.URI;
  36. import java.security.MessageDigest;
  37. import java.security.NoSuchAlgorithmException;
  38. import org.springframework.http.ResponseEntity;
  39. import org.springframework.http.HttpHeaders;
  40. import org.springframework.http.client.ClientHttpRequestInterceptor;
  41. import org.springframework.http.client.ClientHttpResponse;
  42. import org.springframework.web.client.ResponseErrorHandler;
  43. import org.springframework.web.client.RestTemplate;
  44. import lombok.Getter;
  45. import lombok.Setter;
  46. import lombok.extern.slf4j.Slf4j;
  47. import static java.util.Collections.*;
  48. import static java.nio.charset.StandardCharsets.*;
  49. import static org.springframework.http.HttpHeaders.*;

  50. /***********************************************************************************************************************
  51.  *
  52.  * @author  Fabrizio Giudici
  53.  *
  54.  **********************************************************************************************************************/
  55. @Slf4j
  56. public class CachingRestClientSupport
  57.   {
  58.     public enum CacheMode
  59.       {
  60.         /** Always use the network. */
  61.         DONT_USE_CACHE
  62.           {
  63.             @Override @Nonnull
  64.             public ResponseEntity<String> request (@Nonnull final CachingRestClientSupport api,
  65.                                                    @Nonnull final String url)
  66.               throws IOException, InterruptedException
  67.               {
  68.                 return api.requestFromNetwork(url);
  69.               }
  70.           },
  71.         /** Never use the network. */
  72.         ONLY_USE_CACHE
  73.           {
  74.             @Override @Nonnull
  75.             public ResponseEntity<String> request (@Nonnull final CachingRestClientSupport api,
  76.                                                    @Nonnull final String url)
  77.               throws IOException
  78.               {
  79.                 return api.requestFromCache(url).get();
  80.               }
  81.           },
  82.         /** First try the cache, then the network. */
  83.         USE_CACHE
  84.           {
  85.             @Override @Nonnull
  86.             public ResponseEntity<String> request (@Nonnull final CachingRestClientSupport api,
  87.                                                    @Nonnull final String url)
  88.               throws IOException, InterruptedException
  89.               {
  90.                 return api.requestFromCacheAndThenNetwork(url);
  91.               }
  92.           };

  93.         @Nonnull
  94.         public abstract ResponseEntity<String> request (@Nonnull CachingRestClientSupport api,
  95.                                                         @Nonnull String url)
  96.           throws IOException, InterruptedException;
  97.       }

  98.     private final RestTemplate restTemplate = new RestTemplate(); // FIXME: inject?

  99.     @Getter @Setter
  100.     private CacheMode cacheMode = CacheMode.USE_CACHE;

  101.     @Getter @Setter
  102.     private Path cachePath;

  103.     @Getter @Setter
  104.     private String accept = "application/xml";

  105.     @Getter @Setter
  106.     private String userAgent = "blueMarine II (fabrizio.giudici@tidalwave.it)";

  107.     @Getter @Setter
  108.     private long throttleLimit;

  109.     @Getter @Setter @Nonnegative
  110.     private int maxRetry = 3;

  111.     @Getter @Setter
  112.     private List<Integer> retryStatusCodes = List.of(503);

  113.     private long latestNetworkAccessTimestamp = 0;

  114.     /*******************************************************************************************************************
  115.      *
  116.      *
  117.      *
  118.      ******************************************************************************************************************/
  119.     private static final ResponseErrorHandler IGNORE_HTTP_ERRORS = new ResponseErrorHandler()
  120.       {
  121.         @Override
  122.         public boolean hasError (@Nonnull final ClientHttpResponse response)
  123.           throws IOException
  124.           {
  125.             return false;
  126.           }

  127.         @Override
  128.         public void handleError (@Nonnull final ClientHttpResponse response)
  129.           throws IOException
  130.           {
  131.           }
  132.       };

  133.     /*******************************************************************************************************************
  134.      *
  135.      *
  136.      *
  137.      ******************************************************************************************************************/
  138.     private final ClientHttpRequestInterceptor interceptor = (request, body, execution) ->
  139.       {
  140.         final HttpHeaders headers = request.getHeaders();
  141.         headers.add(USER_AGENT, userAgent);
  142.         headers.add(ACCEPT, accept);
  143.         return execution.execute(request, body);
  144.       };

  145.     /*******************************************************************************************************************
  146.      *
  147.      *
  148.      *
  149.      ******************************************************************************************************************/
  150.     public CachingRestClientSupport()
  151.       {
  152.         restTemplate.setInterceptors(singletonList(interceptor));
  153.         restTemplate.setErrorHandler(IGNORE_HTTP_ERRORS);
  154.       }

  155.     /*******************************************************************************************************************
  156.      *
  157.      *
  158.      *
  159.      ******************************************************************************************************************/
  160.     @PostConstruct
  161.     void initialize()
  162.       {
  163.       }

  164.     /*******************************************************************************************************************
  165.      *
  166.      * Performs a  web request.
  167.      *
  168.      * @return                  the response
  169.      *
  170.      ******************************************************************************************************************/
  171.     @Nonnull
  172.     protected ResponseEntity<String> request (@Nonnull final String url)
  173.       throws IOException, InterruptedException
  174.       {
  175.         log.debug("request({})", url);
  176.         return cacheMode.request(this, url);
  177.       }

  178.     /*******************************************************************************************************************
  179.      *
  180.      *
  181.      *
  182.      ******************************************************************************************************************/
  183.     @Nonnull
  184.     private Optional<ResponseEntity<String>> requestFromCache (@Nonnull final String url)
  185.       throws IOException
  186.       {
  187.         log.debug("requestFromCache({})", url);
  188.         return ResponseEntityIo.load(cachePath.resolve(fixedPath(url)));
  189.       }

  190.     /*******************************************************************************************************************
  191.      *
  192.      *
  193.      *
  194.      ******************************************************************************************************************/
  195.     @Nonnull
  196.     private synchronized ResponseEntity<String> requestFromNetwork (@Nonnull final String url)
  197.       throws IOException, InterruptedException
  198.       {
  199.         log.debug("requestFromNetwork({})", url);
  200.         ResponseEntity<String> response = null;

  201.         for (int retry = 0; retry < maxRetry; retry++)
  202.           {
  203.             final long now = System.currentTimeMillis();
  204.             final long delta = now - latestNetworkAccessTimestamp;
  205.             final long toWait = Math.max(throttleLimit - delta, 0);

  206.             if (toWait > 0)
  207.               {
  208.                 log.info(">>>> throttle limit: waiting for {} msec...", toWait);
  209.                 Thread.sleep(toWait);
  210.               }

  211.             latestNetworkAccessTimestamp = now;
  212.             response = restTemplate.getForEntity(URI.create(url), String.class);
  213.             final int httpStatusCode = response.getStatusCodeValue();
  214.             log.debug(">>>> HTTP status code: {}", httpStatusCode);

  215.             if (!retryStatusCodes.contains(httpStatusCode))
  216.               {
  217.                 break;
  218.               }

  219.             log.warn("HTTP status code: {} - retry #{}", httpStatusCode, retry + 1);
  220.           }

  221. //        log.trace(">>>> response: {}", response);
  222.         return response;
  223.       }

  224.     /*******************************************************************************************************************
  225.      *
  226.      *
  227.      *
  228.      ******************************************************************************************************************/
  229.     @Nonnull
  230.     private ResponseEntity<String> requestFromCacheAndThenNetwork (@Nonnull final String url)
  231.       throws IOException, InterruptedException
  232.       {
  233.         log.debug("requestFromCacheAndThenNetwork({})", url);

  234.         return requestFromCache(url).orElseGet(() ->
  235.           {
  236.             try
  237.               {
  238.                 final ResponseEntity<String> response = requestFromNetwork(url);
  239.                 final int httpStatusCode = response.getStatusCodeValue();

  240.                 if (!retryStatusCodes.contains(httpStatusCode))
  241.                   {
  242.                     ResponseEntityIo.store(cachePath.resolve(fixedPath(url)), response, emptyList());
  243.                   }

  244.                 return response;
  245.               }
  246.             catch (IOException | InterruptedException e)
  247.               {
  248.                 throw new RestException(e); // FIXME
  249.               }
  250.           });
  251.       }

  252.     /*******************************************************************************************************************
  253.      *
  254.      *
  255.      ******************************************************************************************************************/
  256.     @Nonnull
  257.     /* package */ static String fixedPath (@Nonnull final String url)
  258.       {
  259.         String s = url.replace("://", "/");
  260.         int i = s.lastIndexOf('/');

  261.         if (i >= 0)
  262.           {
  263.             final String lastSegment = s.substring(i + 1);

  264.             if (lastSegment.length() > 255) // FIXME: and Mac OS X
  265.               {
  266.                 try
  267.                   {
  268.                     final MessageDigest digestComputer = MessageDigest.getInstance("SHA1");
  269.                     s = s.substring(0, i) + "/" + toString(digestComputer.digest(lastSegment.getBytes(UTF_8)));
  270.                   }
  271.                 catch (NoSuchAlgorithmException e)
  272.                   {
  273.                     throw new RuntimeException(e);
  274.                   }
  275.               }
  276.           }

  277.         return s;
  278.       }

  279.     /*******************************************************************************************************************
  280.      *
  281.      *
  282.      ******************************************************************************************************************/
  283.     @Nonnull
  284.     private static String toString (@Nonnull final byte[] bytes)
  285.       {
  286.         final StringBuilder builder = new StringBuilder();

  287.         for (final byte b : bytes)
  288.           {
  289.             final int value = b & 0xff;
  290.             builder.append(Integer.toHexString(value >>> 4)).append(Integer.toHexString(value & 0x0f));
  291.           }

  292.         return builder.toString();
  293.       }
  294.   }