001/*
002 *   Copyright 2020 Vonage
003 *
004 *   Licensed under the Apache License, Version 2.0 (the "License");
005 *   you may not use this file except in compliance with the License.
006 *   You may obtain a copy of the License at
007 *
008 *        http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *   Unless required by applicable law or agreed to in writing, software
011 *   distributed under the License is distributed on an "AS IS" BASIS,
012 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *   See the License for the specific language governing permissions and
014 *   limitations under the License.
015 */
016package com.vonage.client;
017
018import com.vonage.client.auth.AuthMethod;
019import com.vonage.client.logging.LoggingUtils;
020import org.apache.commons.logging.Log;
021import org.apache.commons.logging.LogFactory;
022import org.apache.http.HttpEntity;
023import org.apache.http.HttpEntityEnclosingRequest;
024import org.apache.http.HttpResponse;
025import org.apache.http.client.HttpClient;
026import org.apache.http.client.entity.UrlEncodedFormEntity;
027import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
028import org.apache.http.client.methods.HttpUriRequest;
029import org.apache.http.client.methods.RequestBuilder;
030import org.apache.http.util.EntityUtils;
031
032import java.io.IOException;
033import java.io.UnsupportedEncodingException;
034import java.nio.charset.Charset;
035import java.util.Collections;
036import java.util.HashSet;
037import java.util.Set;
038
039/**
040 * Abstract class to assist in implementing a call against a REST endpoint.
041 * <p>
042 * Concrete implementations must implement {@link #makeRequest(Object)} to construct a {@link RequestBuilder} from the
043 * provided parameterized request object, and {@link #parseResponse(HttpResponse)} to construct the parameterized {@link
044 * HttpResponse} object.
045 * <p>
046 * The REST call is executed by calling {@link #execute(Object)}.
047 *
048 * @param <RequestT> The type of the method-specific request object that will be used to construct an HTTP request
049 * @param <ResultT>  The type of method-specific response object which will be constructed from the returned HTTP
050 *                   response
051 */
052public abstract class AbstractMethod<RequestT, ResultT> implements Method<RequestT, ResultT> {
053    private static final Log LOG = LogFactory.getLog(AbstractMethod.class);
054
055    protected final HttpWrapper httpWrapper;
056    private Set<Class> acceptable;
057
058    public AbstractMethod(HttpWrapper httpWrapper) {
059        this.httpWrapper = httpWrapper;
060    }
061
062    /**
063     * Execute the REST call represented by this method object.
064     *
065     * @param request A RequestT representing input to the REST call to be made
066     *
067     * @return A ResultT representing the response from the executed REST call
068     *
069     * @throws VonageClientException if there is a problem parsing the HTTP response
070     */
071    public ResultT execute(RequestT request) throws VonageResponseParseException, VonageClientException {
072        try {
073            RequestBuilder requestBuilder = applyAuth(makeRequest(request));
074            HttpUriRequest httpRequest = requestBuilder.build();
075
076            // If we have a URL Encoded form entity, we may need to regenerate it as UTF-8
077            // due to a bug (or two!) in RequestBuilder:
078            //
079            // This fix can be removed when HttpClient is upgraded to 4.5, although 4.5 also
080            // has a bug where RequestBuilder.put(uri) and RequestBuilder.post(uri) use the
081            // wrong encoding, whereas RequestBuilder.put().setUri(uri) uses UTF-8.
082            // - MS 2017-04-12
083            if (httpRequest instanceof HttpEntityEnclosingRequest) {
084                HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) httpRequest;
085                HttpEntity entity = entityRequest.getEntity();
086                if (entity instanceof UrlEncodedFormEntity) {
087                    entityRequest.setEntity(new UrlEncodedFormEntity(requestBuilder.getParameters(),
088                            Charset.forName("UTF-8")
089                    ));
090                }
091            }
092            LOG.debug("Request: " + httpRequest);
093            if (LOG.isDebugEnabled() && httpRequest instanceof HttpEntityEnclosingRequestBase) {
094                HttpEntityEnclosingRequestBase enclosingRequest = (HttpEntityEnclosingRequestBase) httpRequest;
095                LOG.debug(EntityUtils.toString(enclosingRequest.getEntity()));
096            }
097            HttpResponse response = this.httpWrapper.getHttpClient().execute(httpRequest);
098
099            LOG.debug("Response: " + LoggingUtils.logResponse(response));
100
101            try{
102                return parseResponse(response);
103            }
104            catch (IOException io){
105                throw new VonageResponseParseException("Unable to parse response.", io);
106            }
107        } catch (UnsupportedEncodingException uee) {
108            throw new VonageUnexpectedException("UTF-8 encoding is not supported by this JVM.", uee);
109        } catch (IOException io) {
110            throw new VonageMethodFailedException("Something went wrong while executing the HTTP request: " +
111                    io.getMessage() + ".", io);
112        }
113    }
114
115    /**
116     * Apply an appropriate authentication method (specified by {@link #getAcceptableAuthMethods()} to the provided
117     * {@link RequestBuilder}, and return the result.
118     *
119     * @param request A RequestBuilder which has not yet had authentication information applied
120     *
121     * @return A RequestBuilder with appropriate authentication information applied (may or not be the same instance as
122     * <pre>request</pre>)
123     *
124     * @throws VonageClientException If no appropriate {@link AuthMethod} is available
125     */
126    protected RequestBuilder applyAuth(RequestBuilder request) throws VonageClientException {
127        return getAuthMethod(getAcceptableAuthMethods()).apply(request);
128    }
129
130    /**
131     * Utility method for obtaining an appropriate {@link AuthMethod} for this call.
132     *
133     * @param acceptableAuthMethods an array of classes, representing authentication methods that are acceptable for
134     *                              this endpoint
135     *
136     * @return An AuthMethod created from one of the provided acceptableAuthMethods.
137     *
138     * @throws VonageClientException If no AuthMethod is available from the provided array of acceptableAuthMethods.
139     */
140    protected AuthMethod getAuthMethod(Class[] acceptableAuthMethods) throws VonageClientException {
141        if (acceptable == null) {
142            this.acceptable = new HashSet<>();
143            Collections.addAll(acceptable, acceptableAuthMethods);
144        }
145
146        return this.httpWrapper.getAuthCollection().getAcceptableAuthMethod(acceptable);
147    }
148
149    public void setHttpClient(HttpClient client) {
150        this.httpWrapper.setHttpClient(client);
151    }
152
153    protected abstract Class[] getAcceptableAuthMethods();
154
155    /**
156     * Construct and return a RequestBuilder instance from the provided request.
157     *
158     * @param request A RequestT representing input to the REST call to be made
159     *
160     * @return A ResultT representing the response from the executed REST call
161     *
162     * @throws UnsupportedEncodingException if UTF-8 encoding is not supported by the JVM
163     */
164    public abstract RequestBuilder makeRequest(RequestT request) throws UnsupportedEncodingException;
165
166    /**
167     * Construct a ResultT representing the contents of the HTTP response returned from the Vonage Voice API.
168     *
169     * @param response An HttpResponse returned from the Vonage Voice API
170     *
171     * @return A ResultT type representing the result of the REST call
172     *
173     * @throws IOException if a problem occurs parsing the response
174     */
175    public abstract ResultT parseResponse(HttpResponse response) throws IOException;
176}