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}