11package com .messagebird ;
22
33import java .io .*;
4+ import java .lang .reflect .Field ;
5+ import java .lang .reflect .Modifier ;
46import java .net .HttpURLConnection ;
57import java .net .Proxy ;
68import java .net .URL ;
2527 * Created by rvt on 1/5/15.
2628 */
2729public class MessageBirdServiceImpl implements MessageBirdService {
30+
2831 private static final String NOT_AUTHORISED_MSG = "You are not authorised for the MessageBird service, please check your access key." ;
2932 private static final String FAILED_DATA_RESPONSE_CODE = "Failed to retrieve data from MessageBird service with response code " ;
3033 private static final String ACCESS_KEY_MUST_BE_SPECIFIED = "Access key must be specified" ;
3134 private static final String SERVICE_URL_MUST_BE_SPECIFIED = "Service URL must be specified" ;
3235 private static final String REQUEST_VALUE_MUST_BE_SPECIFIED = "Request value must be specified" ;
3336 private static final String REQUEST_METHOD_NOT_ALLOWED = "Request method %s is not allowed." ;
37+ private static final String CAN_NOT_ALLOW_PATCH = "Can not set HttpURLConnection.methods field to allow PATCH." ;
38+
39+ private static final String METHOD_DELETE = "DELETE" ;
40+ private static final String METHOD_GET = "GET" ;
41+ private static final String METHOD_PATCH = "PATCH" ;
42+ private static final String METHOD_POST = "POST" ;
3443
35- private static final List <String > REQUEST_METHODS = Arrays .asList ("GET" , "PATCH" , "POST" , "DELETE" );
36- private static final List <String > REQUEST_METHODS_WITH_PAYLOAD = Arrays .asList ("PATCH" , "POST" );
44+ private static final List <String > REQUEST_METHODS = Arrays .asList (METHOD_DELETE , METHOD_GET , METHOD_PATCH , METHOD_POST );
45+ private static final List <String > REQUEST_METHODS_WITH_PAYLOAD = Arrays .asList (METHOD_PATCH , METHOD_POST );
3746 private static final List <String > PROTOCOLS = Arrays .asList (new String []{"http://" , "https://" });
3847
3948 // Used when the actual version can not be parsed.
4049 private static final double DEFAULT_JAVA_VERSION = 0.0 ;
4150
51+ // Indicates whether we've overridden HttpURLConnection's behaviour to
52+ // allow PATCH requests yet. Also see docs on allowPatchRequestsIfNeeded().
53+ private static boolean isPatchRequestAllowed = false ;
54+
4255 private final String accessKey ;
4356 private final String serviceUrl ;
4457 private final String clientVersion = "2.0.0" ;
@@ -191,6 +204,16 @@ protected <P> APIResponse doRequest(final String method, final String url, final
191204 HttpURLConnection connection = null ;
192205 InputStream inputStream = null ;
193206
207+ if (METHOD_PATCH .equalsIgnoreCase (method )) {
208+ // It'd perhaps be cleaner to call this in the constructor, but
209+ // we'd then need to throw GeneralExceptions from there. This means
210+ // it wouldn't be possible to declare AND initialize _instance_
211+ // fields of MessageBirdServiceImpl at the same time. This method
212+ // already throws this exception, so now we don't have to pollute
213+ // our public API further.
214+ allowPatchRequestsIfNeeded ();
215+ }
216+
194217 try {
195218 connection = getConnection (url , payload , method );
196219 int status = connection .getResponseCode ();
@@ -213,6 +236,60 @@ protected <P> APIResponse doRequest(final String method, final String url, final
213236 }
214237 }
215238
239+ /**
240+ * By default, HttpURLConnection does not support PATCH requests. We can
241+ * however work around this with reflection. Many thanks to okutane on
242+ * StackOverflow: https://stackoverflow.com/a/46323891/3521243.
243+ */
244+ private synchronized static void allowPatchRequestsIfNeeded () throws GeneralException {
245+ if (isPatchRequestAllowed ) {
246+ // Don't do anything if we've run this method before. We're in a
247+ // synchronized block, so return ASAP.
248+ return ;
249+ }
250+
251+ try {
252+ // Ensure we can access the fields we need to set.
253+ Field methodsField = HttpURLConnection .class .getDeclaredField ("methods" );
254+ methodsField .setAccessible (true );
255+
256+ Field modifiersField = Field .class .getDeclaredField ("modifiers" );
257+ modifiersField .setAccessible (true );
258+ modifiersField .setInt (methodsField , methodsField .getModifiers () & ~Modifier .FINAL );
259+
260+ Object noInstanceBecauseStaticField = null ;
261+
262+ // Determine what methods should be allowed.
263+ String [] existingMethods = (String []) methodsField .get (noInstanceBecauseStaticField );
264+ String [] allowedMethods = getAllowedMethods (existingMethods );
265+
266+ // Override the actual field to allow PATCH.
267+ methodsField .set (noInstanceBecauseStaticField , allowedMethods );
268+
269+ // Set flag so we only have to run this once.
270+ isPatchRequestAllowed = true ;
271+ } catch (IllegalAccessException | NoSuchFieldException e ) {
272+ throw new GeneralException (CAN_NOT_ALLOW_PATCH );
273+ }
274+ }
275+
276+ /**
277+ * Appends PATCH to the provided array.
278+ *
279+ * @param existingMethods Methods that are, and must be, allowed.
280+ * @return New array also containing PATCH.
281+ */
282+ private static String [] getAllowedMethods (String [] existingMethods ) {
283+ int listCapacity = existingMethods .length + 1 ;
284+
285+ List <String > allowedMethods = new ArrayList <>(listCapacity );
286+
287+ allowedMethods .addAll (Arrays .asList (existingMethods ));
288+ allowedMethods .add (METHOD_PATCH );
289+
290+ return allowedMethods .toArray (new String [0 ]);
291+ }
292+
216293 /**
217294 * Reads the stream until it has no more bytes and returns a UTF-8 encoded
218295 * string representation.
@@ -277,14 +354,7 @@ public <P> HttpURLConnection getConnection(final String serviceUrl, final P post
277354 connection .setRequestProperty ("User-agent" , userAgentString );
278355
279356 if ("POST" .equals (requestType ) || "PATCH" .equals (requestType )) {
280- if ("PATCH" .equals (requestType )) {
281- // HttpURLConnection does not support PATCH so we'll send a
282- // POST, but instruct the server to interpret it as a PATCH.
283- // See: https://stackoverflow.com/a/32503192/3521243
284- connection .setRequestProperty ("X-HTTP-Method-Override" , "PATCH" );
285- }
286-
287- connection .setRequestMethod ("POST" );
357+ connection .setRequestMethod (requestType );
288358 connection .setDoOutput (true );
289359 connection .setRequestProperty ("Content-Type" , "application/json" );
290360 ObjectMapper mapper = new ObjectMapper ();
@@ -448,6 +518,4 @@ private String getPathVariables(final Map<String, Object> map) {
448518 }
449519 return bpath .toString ();
450520 }
451-
452-
453- }
521+ }
0 commit comments