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