View Javadoc
1   /*
2    * Copyright (c) 2023. Roland T. Lichti, Kaiserpfalz EDV-Service.
3    *
4    * This program is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * This program is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU General Public License for more details.
13   *
14   * You should have received a copy of the GNU General Public License
15   * along with this program.  If not, see <https://www.gnu.org/licenses/>.
16   */
17  
18  package de.kaiserpfalzedv.services.eansearch.filter;
19  
20  import java.time.OffsetDateTime;
21  import java.time.ZoneOffset;
22  import java.time.temporal.ChronoUnit;
23  
24  import org.springframework.beans.factory.annotation.Autowired;
25  
26  import de.kaiserpfalzedv.services.eansearch.mapper.EanSearchException;
27  import de.kaiserpfalzedv.services.eansearch.mapper.EanSearchTooManyRequestsException;
28  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
29  import feign.InvocationContext;
30  import feign.RequestInterceptor;
31  import feign.RequestTemplate;
32  import feign.ResponseInterceptor;
33  import io.micrometer.core.instrument.Counter;
34  import io.micrometer.core.instrument.MeterRegistry;
35  import io.micrometer.core.instrument.Tags;
36  import jakarta.annotation.PostConstruct;
37  import jakarta.annotation.PreDestroy;
38  import jakarta.inject.Singleton;
39  import lombok.RequiredArgsConstructor;
40  import lombok.extern.slf4j.Slf4j;
41  
42  /**
43   * <p>RequestLimitFilter -- Reports the used credits and the remaining credits for the EAN search api.</p>
44   *
45   * @author klenkes74 {@literal <rlichti@kaiserpfalz-edv.de>}
46   * @since 1.0.0  2023-01-17
47   */
48  @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "lombok created constructor used.")
49  @Singleton
50  @RequiredArgsConstructor(onConstructor = @__(@Autowired))
51  @Slf4j
52  public class RequestLimitFilter implements RequestInterceptor, ResponseInterceptor {
53      private static final String API_REMAINING_REQUEST_HEADER = "X-Credits-Remaining";
54      private static final String API_REMAINING_METRICS_NAME = "ean-search.credits.remaining";
55      private static final String API_REQUESTS_HANDLED_SINCE_START = "ean-search.credits.used-since-start";
56  
57      private final MeterRegistry registry;
58      private Counter requestCounter;
59  
60      // No lombok generateion to make it synchronized
61      private int remaining = -1;
62      private OffsetDateTime lastRequest = OffsetDateTime.now(ZoneOffset.UTC).minus(1, ChronoUnit.DAYS);
63  
64      @PostConstruct
65      public void registerMetric() {
66          this.requestCounter = this.registry.counter(API_REQUESTS_HANDLED_SINCE_START, Tags.empty());
67      }
68  
69      @PreDestroy
70      public void unregisterMetrics() {
71          this.requestCounter.close();
72      }
73  
74      public synchronized int getRemaining() {
75          return this.remaining;
76      }
77  
78      /**
79       * Resets the counter and enables new requests, that would otherwise being blocked by this filter.
80       */
81      public void reset() {
82          synchronized(this) {
83              this.remaining = -1;
84              log.info("EAN Search credit reporter reset. Requests should now result in requests to the API.");
85          }
86      }
87  
88  
89  
90      /**
91       * Reports the handled requests and remaining credits to the API.
92       *
93       * @param requestContext  request context.
94       * @param responseContext response context.
95       */
96      @Override
97      public Object intercept(final InvocationContext context, final Chain chain) throws Exception {
98          final String remaining = context.response().headers().get(API_REMAINING_REQUEST_HEADER).stream().findFirst().orElse(null);
99  
100         synchronized (this) {
101             if (remaining != null) {
102                 this.remaining = Integer.valueOf(remaining, 10);
103                 this.registry.gauge(API_REMAINING_METRICS_NAME, Tags.empty(), this.remaining);
104             }
105 
106             log.debug("EAN-Search remaining requests. remaining={}, used={}", this.remaining, this.requestCounter.count());
107         }
108 
109         return chain.next(context);
110     }
111 
112     /**
113      * This filter prevents the external call when there are no credits left for this API.
114      *
115      * @param requestContext request context.
116      * @throws EanSearchException When the credits for this API has been used and there are no
117      *                                               credits remaining.
118      */
119     @Override
120     public void apply(final RequestTemplate request) throws EanSearchException {
121         if (
122                 this.remaining == 0
123                 && this.lastRequestWasToday()
124         ) {
125             log.error("Can't search for EAN any more. There is no remaining credit left. lastRequest='{}',", this.lastRequest);
126 
127             throw new EanSearchTooManyRequestsException();
128         }
129 
130         synchronized (this) {
131             this.lastRequest = OffsetDateTime.now(ZoneOffset.UTC);
132         }
133     }
134 
135     private boolean lastRequestWasToday() {
136         return this.lastRequest.isAfter(
137                 OffsetDateTime.now(ZoneOffset.UTC).toLocalDate()
138                         .atStartOfDay(ZoneOffset.UTC)
139                         .toOffsetDateTime()
140         );
141     }
142 }