001package ball.http; 002/*- 003 * ########################################################################## 004 * Web API Client (HTTP) Utilities 005 * $Id: ProtocolClient.java 6118 2020-06-04 19:31:45Z ball $ 006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-http/trunk/src/main/java/ball/http/ProtocolClient.java $ 007 * %% 008 * Copyright (C) 2016 - 2020 Allen D. Ball 009 * %% 010 * Licensed under the Apache License, Version 2.0 (the "License"); 011 * you may not use this file except in compliance with the License. 012 * You may obtain a copy of the License at 013 * 014 * http://www.apache.org/licenses/LICENSE-2.0 015 * 016 * Unless required by applicable law or agreed to in writing, software 017 * distributed under the License is distributed on an "AS IS" BASIS, 018 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 019 * See the License for the specific language governing permissions and 020 * limitations under the License. 021 * ########################################################################## 022 */ 023import ball.http.annotation.Protocol; 024import com.fasterxml.jackson.databind.ObjectMapper; 025import java.io.IOException; 026import java.lang.annotation.Annotation; 027import java.lang.reflect.AnnotatedElement; 028import java.lang.reflect.Method; 029import java.lang.reflect.Proxy; 030import java.nio.charset.Charset; 031import javax.xml.bind.JAXBContext; 032import javax.xml.bind.JAXBException; 033import javax.xml.bind.Marshaller; 034import javax.xml.bind.Unmarshaller; 035import lombok.ToString; 036import org.apache.http.HttpRequest; 037import org.apache.http.HttpRequestInterceptor; 038import org.apache.http.HttpResponse; 039import org.apache.http.HttpResponseInterceptor; 040import org.apache.http.client.HttpClient; 041import org.apache.http.impl.client.CloseableHttpClient; 042import org.apache.http.impl.client.HttpClientBuilder; 043import org.apache.http.protocol.HttpContext; 044import org.apache.http.protocol.HttpCoreContext; 045 046import static java.util.Objects.requireNonNull; 047 048/** 049 * Abstract {@link ProtocolClient} base class. 050 * 051 * @param <P> The protocol type erasure. 052 * <p> 053 * This class provides: 054 * <ol> 055 * <li value="1"> 056 * {@link HttpClient} ({@link #client()}) 057 * </li> 058 * <li value="2"> 059 * {@link HttpContext} ({@link #context()}) 060 * </li> 061 * <li value="3"> 062 * A {@link Proxy} which implements the annotated protocol interface 063 * </li> 064 * <li value="4"> 065 * {@link.this} implements {@link HttpRequestInterceptor} 066 * and {@link HttpResponseInterceptor} which are configured into 067 * {@link HttpClientBuilder}; subclasses can override 068 * {@link #process(HttpRequest,HttpContext)} and 069 * {@link #process(HttpResponse,HttpContext)} 070 * </li> 071 * </ol> 072 * <p> 073 * See the {@link ProtocolRequestBuilder} for the supported protocol 074 * interface {@link Annotation}s and method parameter types. 075 * </p> 076 * <p> 077 * See {@link ProtocolRequestBuilder} and 078 * {@link ProtocolInvocationHandler} for a description of how 079 * {@link HttpRequest}s are generated and executed. 080 * </p> 081 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball} 082 * @version $Revision: 6118 $ 083 */ 084@ToString 085public abstract class ProtocolClient<P> implements HttpRequestInterceptor, 086 HttpResponseInterceptor { 087 private final CloseableHttpClient client; 088 private final HttpCoreContext context; 089 private final Class<? extends P> protocol; 090 private final Object proxy; 091 092 /** 093 * Field exposed for subclass initialization; 094 * see {@link #getCharset()}. 095 */ 096 protected transient Charset charset = null; 097 098 /** 099 * Field exposed for subclass initialization; 100 * see {@link #getJAXBContext()}. 101 */ 102 protected transient JAXBContext jaxb = null; 103 104 /** 105 * Field exposed for subclass initialization; 106 * see {@link #getObjectMapper()}. 107 */ 108 protected transient ObjectMapper mapper = null; 109 110 private transient Marshaller marshaller = null; 111 private transient Unmarshaller unmarshaller = null; 112 113 /** 114 * Constructor that creates {@link HttpClientBuilder} 115 * and {@link HttpCoreContext}. 116 * 117 * @param protocol The protocol {@link Class}. 118 */ 119 protected ProtocolClient(Class<? extends P> protocol) { 120 this(HttpClientBuilder.create(), null, protocol); 121 } 122 123 /** 124 * Constructor that allows the subclass to provide a configured 125 * {@link HttpClientBuilder} and/or {@link HttpCoreContext}. 126 * 127 * @param builder A configured {@link HttpClientBuilder}. 128 * @param context A {@link HttpCoreContext} (may be 129 * {@code null}). 130 * @param protocol The protocol {@link Class}. 131 */ 132 protected ProtocolClient(HttpClientBuilder builder, 133 HttpCoreContext context, 134 Class<? extends P> protocol) { 135 this.client = 136 builder 137 .addInterceptorLast((HttpRequestInterceptor) this) 138 .addInterceptorLast((HttpResponseInterceptor) this) 139 .build(); 140 this.context = (context != null) ? context : HttpCoreContext.create(); 141 this.protocol = requireNonNull(protocol, "protocol"); 142 this.proxy = 143 Proxy.newProxyInstance(protocol.getClassLoader(), 144 new Class<?>[] { protocol }, 145 new ProtocolInvocationHandler(this)); 146 } 147 148 /** 149 * @return {@link ProtocolClient} {@link CloseableHttpClient} 150 */ 151 public CloseableHttpClient client() { return client; } 152 153 /** 154 * @return {@link ProtocolClient} {@link HttpCoreContext} 155 */ 156 public HttpCoreContext context() { return context; } 157 158 /** 159 * @return {@link #protocol()} {@link Class} 160 */ 161 public Class<? extends P> protocol() { return protocol; } 162 163 /** 164 * @return {@link #protocol()} {@link Proxy} 165 */ 166 public P proxy() { return protocol.cast(proxy); } 167 168 /** 169 * @return {@link Proxy} {@link ProtocolInvocationHandler} 170 */ 171 public ProtocolInvocationHandler handler() { 172 return (ProtocolInvocationHandler) Proxy.getInvocationHandler(proxy()); 173 } 174 175 /** 176 * @return {@link #protocol()} configured {@link Charset} 177 */ 178 public Charset getCharset() { 179 synchronized (this) { 180 if (charset == null) { 181 String name = 182 (String) getDefaultedValueOf(protocol(), 183 Protocol.class, "charset"); 184 185 charset = Charset.forName(name); 186 } 187 } 188 189 return charset; 190 } 191 192 private Object getDefaultedValueOf(AnnotatedElement element, 193 Class<? extends Annotation> type, 194 String name) { 195 Object object = null; 196 197 try { 198 Method method = type.getMethod(name); 199 200 if (object == null) { 201 Annotation annotation = element.getAnnotation(type); 202 203 if (annotation != null) { 204 object = method.invoke(annotation); 205 } 206 } 207 208 if (object == null) { 209 object = method.getDefaultValue(); 210 } 211 } catch (Exception exception) { 212 throw new IllegalStateException(exception); 213 } 214 215 return object; 216 } 217 218 /** 219 * @return {@link #protocol()} configured {@link JAXBContext} 220 */ 221 public JAXBContext getJAXBContext() { 222 if (jaxb == null) { 223 synchronized(this) { 224 if (jaxb == null) { 225 try { 226 jaxb = 227 JAXBContext.newInstance(new Class<?>[] { protocol() }); 228 } catch (JAXBException exception) { 229 throw new IllegalStateException(exception); 230 } 231 } 232 } 233 } 234 235 return jaxb; 236 } 237 238 /** 239 * @return {@link #protocol()} configured {@link Marshaller} 240 */ 241 public Marshaller getMarshaller() { 242 if (marshaller == null) { 243 synchronized(this) { 244 if (marshaller == null) { 245 try { 246 marshaller = getJAXBContext().createMarshaller(); 247 marshaller.setProperty(Marshaller.JAXB_ENCODING, 248 getCharset().name()); 249 } catch (JAXBException exception) { 250 throw new IllegalStateException(exception); 251 } 252 } 253 } 254 } 255 256 return marshaller; 257 } 258 259 /** 260 * @return {@link #protocol()} configured {@link Unmarshaller} 261 */ 262 public Unmarshaller getUnmarshaller() { 263 if (unmarshaller == null) { 264 synchronized(this) { 265 if (unmarshaller == null) { 266 try { 267 unmarshaller = getJAXBContext().createUnmarshaller(); 268 } catch (JAXBException exception) { 269 throw new IllegalStateException(exception); 270 } 271 } 272 } 273 } 274 275 return unmarshaller; 276 } 277 278 /** 279 * @return {@link #protocol()} configured {@link ObjectMapper}. 280 */ 281 public ObjectMapper getObjectMapper() { 282 if (mapper == null) { 283 synchronized(this) { 284 if (mapper == null) { 285 mapper = new ObjectMapper(); 286 } 287 } 288 } 289 290 return mapper; 291 } 292 293 @Override 294 public void process(HttpRequest request, 295 HttpContext context) throws IOException { 296 } 297 298 @Override 299 public void process(HttpResponse response, 300 HttpContext context) throws IOException { 301 } 302}