001/* 002 * Copyright (c) 2011-2017 Nexmo Inc 003 * 004 * Permission is hereby granted, free of charge, to any person obtaining a copy 005 * of this software and associated documentation files (the "Software"), to deal 006 * in the Software without restriction, including without limitation the rights 007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 008 * copies of the Software, and to permit persons to whom the Software is 009 * furnished to do so, subject to the following conditions: 010 * 011 * The above copyright notice and this permission notice shall be included in 012 * all copies or substantial portions of the Software. 013 * 014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 020 * THE SOFTWARE. 021 */ 022package com.nexmo.client.voice.endpoints; 023 024import com.nexmo.client.HttpWrapper; 025import com.nexmo.client.NexmoClientException; 026import com.nexmo.client.NexmoMethodFailedException; 027import com.nexmo.client.NexmoUnexpectedException; 028import com.nexmo.client.auth.AuthMethod; 029import org.apache.commons.logging.Log; 030import org.apache.commons.logging.LogFactory; 031import org.apache.http.HttpEntity; 032import org.apache.http.HttpEntityEnclosingRequest; 033import org.apache.http.HttpResponse; 034import org.apache.http.client.HttpClient; 035import org.apache.http.client.entity.UrlEncodedFormEntity; 036import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; 037import org.apache.http.client.methods.HttpUriRequest; 038import org.apache.http.client.methods.RequestBuilder; 039import org.apache.http.util.EntityUtils; 040 041import java.io.IOException; 042import java.io.UnsupportedEncodingException; 043import java.nio.charset.Charset; 044import java.util.HashSet; 045import java.util.Set; 046 047/** 048 * Abstract class to assist in implementing a call against a REST endpoint. 049 * <p> 050 * Concrete implementations must implement {@link #makeRequest(Object)} to construct a {@link RequestBuilder} from the 051 * provided parameterized request object, and {@link #parseResponse(HttpResponse)} to construct the parameterized 052 * {@link HttpResponse} object. 053 * <p> 054 * The REST call is executed by calling {@link #execute(Object)}. 055 * 056 * @param <RequestT> The type of the method-specific request object that will be used to construct an HTTP request 057 * @param <ResultT> The type of method-specific response object which will be constructed from the returned 058 * HTTP response 059 */ 060public abstract class AbstractMethod<RequestT, ResultT> implements Method<RequestT, ResultT> { 061 private static final Log LOG = LogFactory.getLog(AbstractMethod.class); 062 063 private final HttpWrapper httpWrapper; 064 private Set<Class> acceptable; 065 066 public AbstractMethod(HttpWrapper httpWrapper) { 067 this.httpWrapper = httpWrapper; 068 } 069 070 /** 071 * Execute the REST call represented by this method object. 072 * 073 * @param request A RequestT representing input to the REST call to be made 074 * @return A ResultT representing the response from the executed REST call 075 * @throws IOException if an exception occurs making the REST call 076 * @throws NexmoClientException if there is a problem parsing the HTTP response 077 */ 078 // TODO: Consider wrapping IOException in a nexmo-specific transport exception. 079 public ResultT execute(RequestT request) throws IOException, NexmoClientException { 080 try { 081 RequestBuilder requestBuilder = applyAuth(makeRequest(request)); 082 HttpUriRequest httpRequest = requestBuilder.build(); 083 084 // If we have a URL Encoded form entity, we may need to regenerate it as UTF-8 085 // due to a bug (or two!) in RequestBuilder: 086 // 087 // This fix can be removed when HttpClient is upgraded to 4.5, although 4.5 also 088 // has a bug where RequestBuilder.put(uri) and RequestBuilder.post(uri) use the 089 // wrong encoding, whereas RequestBuilder.put().setUri(uri) uses UTF-8. 090 // - MS 2017-04-12 091 if (httpRequest instanceof HttpEntityEnclosingRequest) { 092 HttpEntityEnclosingRequest entityRequest = 093 (HttpEntityEnclosingRequest) httpRequest; 094 HttpEntity entity = entityRequest.getEntity(); 095 if (entity != null && entity instanceof UrlEncodedFormEntity) { 096 entityRequest.setEntity(new UrlEncodedFormEntity( 097 requestBuilder.getParameters(), 098 Charset.forName("UTF-8"))); 099 } 100 } 101 LOG.debug("Request: " + httpRequest); 102 if (LOG.isDebugEnabled() && httpRequest instanceof HttpEntityEnclosingRequestBase) { 103 HttpEntityEnclosingRequestBase enclosingRequest = (HttpEntityEnclosingRequestBase) httpRequest; 104 LOG.debug(EntityUtils.toString(enclosingRequest.getEntity())); 105 } 106 HttpResponse response = this.httpWrapper.getHttpClient().execute(httpRequest); 107 return parseResponse(response); 108 } catch (UnsupportedEncodingException uee) { 109 throw new NexmoUnexpectedException("UTF-8 encoding is not supported by this JVM.", uee); 110 } 111 } 112 113 /** 114 * Apply an appropriate authentication method (specified by {@link #getAcceptableAuthMethods()} to the provided 115 * {@link RequestBuilder}, and return the result. 116 * 117 * @param request A RequestBuilder which has not yet had authentication information applied 118 * @return A RequestBuilder with appropriate authentication information applied (may or not be the same instance as 119 * <pre>request</pre>) 120 * @throws NexmoClientException If no appropriate {@link AuthMethod} is available 121 */ 122 protected RequestBuilder applyAuth(RequestBuilder request) throws NexmoClientException { 123 return getAuthMethod(getAcceptableAuthMethods()).apply(request); 124 } 125 126 /** 127 * Utility method for obtaining an appropriate {@link AuthMethod} for this call. 128 * 129 * @param acceptableAuthMethods an array of classes, representing authentication methods that are acceptable for 130 * this endpoint 131 * @return An AuthMethod created from one of the provided acceptableAuthMethods. 132 * @throws NexmoClientException If no AuthMethod is available from the provided array of acceptableAuthMethods. 133 */ 134 protected AuthMethod getAuthMethod(Class[] acceptableAuthMethods) throws NexmoClientException { 135 if (acceptable == null) { 136 this.acceptable = new HashSet<>(); 137 for (Class c : acceptableAuthMethods) { 138 acceptable.add(c); 139 } 140 } 141 142 return this.httpWrapper.getAuthCollection().getAcceptableAuthMethod(acceptable); 143 } 144 145 public void setHttpClient(HttpClient client) { 146 this.httpWrapper.setHttpClient(client); 147 } 148 149 protected abstract Class[] getAcceptableAuthMethods(); 150 151 /** 152 * Construct and return a RequestBuilder instance from the provided request. 153 * 154 * @param request A RequestT representing input to the REST call to be made 155 * @return A ResultT representing the response from the executed REST call 156 * @throws NexmoClientException if a problem is encountered constructing the request or response. 157 * @throws UnsupportedEncodingException if UTF-8 encoding is not supported by the JVM 158 */ 159 public abstract RequestBuilder makeRequest(RequestT request) 160 throws NexmoClientException, UnsupportedEncodingException; 161 162 /** 163 * Construct a ResultT representing the contents of the HTTP response returned from the Nexmo Voice API. 164 * 165 * @param response An HttpResponse returned from the Nexmo Voice API 166 * @return A ResultT type representing the result of the REST call 167 * @throws IOException if a problem occurs parsing the response 168 */ 169 public abstract ResultT parseResponse(HttpResponse response) throws IOException, NexmoClientException; 170}