Skip to content

Commit 8bb69dc

Browse files
authored
Merge pull request #44 from messagebird/add-patch-httpurlconnection
Override HttpURLConnection behaviour to allow PATCH requests.
2 parents fbae136 + bf8d507 commit 8bb69dc

File tree

1 file changed

+81
-13
lines changed

1 file changed

+81
-13
lines changed

api/src/main/java/com/messagebird/MessageBirdServiceImpl.java

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.messagebird;
22

33
import java.io.*;
4+
import java.lang.reflect.Field;
5+
import java.lang.reflect.Modifier;
46
import java.net.HttpURLConnection;
57
import java.net.Proxy;
68
import java.net.URL;
@@ -25,20 +27,31 @@
2527
* Created by rvt on 1/5/15.
2628
*/
2729
public 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

Comments
 (0)