1
1
package com .messagebird ;
2
2
3
3
import java .io .*;
4
+ import java .lang .reflect .Field ;
5
+ import java .lang .reflect .Modifier ;
4
6
import java .net .HttpURLConnection ;
5
7
import java .net .Proxy ;
6
8
import java .net .URL ;
25
27
* Created by rvt on 1/5/15.
26
28
*/
27
29
public class MessageBirdServiceImpl implements MessageBirdService {
30
+
28
31
private static final String NOT_AUTHORISED_MSG = "You are not authorised for the MessageBird service, please check your access key." ;
29
32
private static final String FAILED_DATA_RESPONSE_CODE = "Failed to retrieve data from MessageBird service with response code " ;
30
33
private static final String ACCESS_KEY_MUST_BE_SPECIFIED = "Access key must be specified" ;
31
34
private static final String SERVICE_URL_MUST_BE_SPECIFIED = "Service URL must be specified" ;
32
35
private static final String REQUEST_VALUE_MUST_BE_SPECIFIED = "Request value must be specified" ;
33
36
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" ;
34
43
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 );
37
46
private static final List <String > PROTOCOLS = Arrays .asList (new String []{"http://" , "https://" });
38
47
39
48
// Used when the actual version can not be parsed.
40
49
private static final double DEFAULT_JAVA_VERSION = 0.0 ;
41
50
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
+
42
55
private final String accessKey ;
43
56
private final String serviceUrl ;
44
57
private final String clientVersion = "2.0.0" ;
@@ -191,6 +204,16 @@ protected <P> APIResponse doRequest(final String method, final String url, final
191
204
HttpURLConnection connection = null ;
192
205
InputStream inputStream = null ;
193
206
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
+
194
217
try {
195
218
connection = getConnection (url , payload , method );
196
219
int status = connection .getResponseCode ();
@@ -213,6 +236,60 @@ protected <P> APIResponse doRequest(final String method, final String url, final
213
236
}
214
237
}
215
238
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
+
216
293
/**
217
294
* Reads the stream until it has no more bytes and returns a UTF-8 encoded
218
295
* string representation.
@@ -277,14 +354,7 @@ public <P> HttpURLConnection getConnection(final String serviceUrl, final P post
277
354
connection .setRequestProperty ("User-agent" , userAgentString );
278
355
279
356
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 );
288
358
connection .setDoOutput (true );
289
359
connection .setRequestProperty ("Content-Type" , "application/json" );
290
360
ObjectMapper mapper = new ObjectMapper ();
@@ -448,6 +518,4 @@ private String getPathVariables(final Map<String, Object> map) {
448
518
}
449
519
return bpath .toString ();
450
520
}
451
-
452
-
453
- }
521
+ }
0 commit comments