View Javadoc
1   /*
2    * Copyright (C) 2023 B3Partners B.V.
3    *
4    * SPDX-License-Identifier: MIT
5    */
6   package org.tailormap.api.geotools;
7   
8   import java.io.ByteArrayOutputStream;
9   import java.io.IOException;
10  import java.io.InputStream;
11  import java.net.URL;
12  import java.util.Collections;
13  import java.util.HashMap;
14  import java.util.Map;
15  import java.util.Set;
16  import java.util.function.BiConsumer;
17  import org.geotools.http.HTTPClient;
18  import org.geotools.http.HTTPResponse;
19  
20  /**
21   * Wrapper for a GeoTools HTTPClient that allows access to the response headers and body after the
22   * original response has been consumed and disposed of by other code. The response body for the
23   * latest request is kept in memory, a consumer can be specified to get responses of earlier
24   * requests. Response headers of an unwrapped HTTPClient can only be retrieved when the response
25   * isn't disposed, this wrapper allows response headers to be cached for retrieval even after
26   * disposal.
27   */
28  public class ResponseTeeingHTTPClient implements HTTPClient {
29  
30    public class TeeHTTPResponseWrapper implements HTTPResponse {
31      private final HTTPResponse wrapped;
32  
33      private boolean disposed = false;
34      private final ByteArrayOutputStream copy = new ByteArrayOutputStream();
35  
36      private String contentType;
37      private final Map<String, String> cachedResponseHeaders = new HashMap<>();
38  
39      public TeeHTTPResponseWrapper(HTTPResponse wrapped) {
40        this.wrapped = wrapped;
41      }
42  
43      public byte[] getCopy() {
44        return copy.toByteArray();
45      }
46  
47      // <editor-fold desc="methods directly delegated to wrapped object">
48      @Override
49      public void dispose() {
50        disposed = true;
51        wrapped.dispose();
52      }
53  
54      public boolean isDisposed() {
55        return disposed;
56      }
57  
58      @Override
59      public String getContentType() {
60        if (contentType == null) {
61          contentType = wrapped.getContentType();
62        }
63        return contentType;
64      }
65  
66      @Override
67      public String getResponseHeader(String header) {
68        if (cachedResponseHeaders.containsKey(header)) {
69          return cachedResponseHeaders.get(header);
70        }
71        // When disposed, the wrapped client will probably throw a NPE when trying to get a response
72        // header
73        if (isDisposed()) {
74          return null;
75        }
76        return wrapped.getResponseHeader(header);
77      }
78  
79      @Override
80      public InputStream getResponseStream() throws IOException {
81        // Cache response headers now, when the internal connection is still available
82        for (String header : responseHeadersToCache) {
83          cachedResponseHeaders.put(header, wrapped.getResponseHeader(header));
84        }
85        return new org.apache.commons.io.input.TeeInputStream(wrapped.getResponseStream(), copy);
86      }
87  
88      @Override
89      public String getResponseCharset() {
90        return wrapped.getResponseCharset();
91      }
92      // </editor-fold>
93    }
94  
95    private TeeHTTPResponseWrapper responseWrapper;
96  
97    private final HTTPClient wrapped;
98  
99    private final BiConsumer<URL, TeeHTTPResponseWrapper> requestConsumer;
100 
101   private final Set<String> responseHeadersToCache;
102 
103   /**
104    * Wrap a GeoTools HTTPClient allowing access to the latest response body after it has been
105    * consumed by handing the client to a GeoTools module. Note that getting the response headers
106    * after the original response has been consumed will return null without explicitly calling the
107    * other constructor with the responseHeadersToCache parameter.
108    *
109    * @param wrapped The GeoTools HTTPClient to wrap
110    */
111   public ResponseTeeingHTTPClient(HTTPClient wrapped) {
112     this(wrapped, null, null);
113   }
114 
115   /**
116    * Wrap a GeoTools HTTPClient allowing access to the all responses after they have been consumed
117    * by handing the client to a GeoTools module by passing a consumer for each request URL and
118    * wrapped response. The responseHeadersToCache parameter allows access to response headers after
119    * the original wrapped response has been disposed (only cached when getResponseStream() is
120    * requested).
121    *
122    * @param wrapped The GeoTools HTTPClient to wrap
123    * @param requestConsumer Consumer for each request so not only the latest response can be
124    *     accessed, may be null
125    * @param responseHeadersToCache Which response headers to cache, may be null
126    */
127   public ResponseTeeingHTTPClient(
128       HTTPClient wrapped,
129       BiConsumer<URL, TeeHTTPResponseWrapper> requestConsumer,
130       Set<String> responseHeadersToCache) {
131     this.wrapped = wrapped;
132     this.requestConsumer = requestConsumer == null ? (url, response) -> {} : requestConsumer;
133     this.responseHeadersToCache =
134         responseHeadersToCache == null ? Collections.emptySet() : responseHeadersToCache;
135   }
136 
137   @Override
138   public HTTPResponse get(URL url, Map<String, String> headers) throws IOException {
139     this.responseWrapper = new TeeHTTPResponseWrapper(wrapped.get(url, headers));
140     requestConsumer.accept(url, this.responseWrapper);
141     return responseWrapper;
142   }
143 
144   @Override
145   public HTTPResponse post(URL url, InputStream inputStream, String s) throws IOException {
146     this.responseWrapper = new TeeHTTPResponseWrapper(wrapped.post(url, inputStream, s));
147     requestConsumer.accept(url, this.responseWrapper);
148     return responseWrapper;
149   }
150 
151   @Override
152   public HTTPResponse get(URL url) throws IOException {
153     this.responseWrapper = new TeeHTTPResponseWrapper(wrapped.get(url));
154     requestConsumer.accept(url, this.responseWrapper);
155     return responseWrapper;
156   }
157 
158   public HTTPResponse getLatestResponse() {
159     return responseWrapper;
160   }
161 
162   public byte[] getLatestResponseCopy() {
163     return responseWrapper == null ? null : responseWrapper.getCopy();
164   }
165 
166   // <editor-fold desc="methods directly delegated to wrapped object">
167   @Override
168   public String getUser() {
169     return wrapped.getUser();
170   }
171 
172   @Override
173   public void setUser(String user) {
174     wrapped.setUser(user);
175   }
176 
177   @Override
178   public String getPassword() {
179     return wrapped.getPassword();
180   }
181 
182   @Override
183   public void setPassword(String password) {
184     wrapped.setPassword(password);
185   }
186 
187   @Override
188   public Map<String, String> getExtraParams() {
189     return wrapped.getExtraParams();
190   }
191 
192   @Override
193   public void setExtraParams(Map<String, String> extraParams) {
194     wrapped.setExtraParams(extraParams);
195   }
196 
197   @Override
198   public int getConnectTimeout() {
199     return wrapped.getConnectTimeout();
200   }
201 
202   @Override
203   public void setConnectTimeout(int connectTimeout) {
204     wrapped.setConnectTimeout(connectTimeout);
205   }
206 
207   @Override
208   public int getReadTimeout() {
209     return wrapped.getReadTimeout();
210   }
211 
212   @Override
213   public void setReadTimeout(int readTimeout) {
214     wrapped.setReadTimeout(readTimeout);
215   }
216 
217   @Override
218   public void setTryGzip(boolean tryGZIP) {
219     wrapped.setTryGzip(tryGZIP);
220   }
221 
222   @Override
223   public boolean isTryGzip() {
224     return wrapped.isTryGzip();
225   }
226   // </editor-fold>
227 }