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