Skip to content

Commit

Permalink
Merge pull request #44 from messagebird/add-patch-httpurlconnection
Browse files Browse the repository at this point in the history
Override HttpURLConnection behaviour to allow PATCH requests.
  • Loading branch information
dysosmus authored Oct 1, 2018
2 parents fbae136 + bf8d507 commit 8bb69dc
Showing 1 changed file with 81 additions and 13 deletions.
94 changes: 81 additions & 13 deletions api/src/main/java/com/messagebird/MessageBirdServiceImpl.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.messagebird;

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
Expand All @@ -25,20 +27,31 @@
* Created by rvt on 1/5/15.
*/
public class MessageBirdServiceImpl implements MessageBirdService {

private static final String NOT_AUTHORISED_MSG = "You are not authorised for the MessageBird service, please check your access key.";
private static final String FAILED_DATA_RESPONSE_CODE = "Failed to retrieve data from MessageBird service with response code ";
private static final String ACCESS_KEY_MUST_BE_SPECIFIED = "Access key must be specified";
private static final String SERVICE_URL_MUST_BE_SPECIFIED = "Service URL must be specified";
private static final String REQUEST_VALUE_MUST_BE_SPECIFIED = "Request value must be specified";
private static final String REQUEST_METHOD_NOT_ALLOWED = "Request method %s is not allowed.";
private static final String CAN_NOT_ALLOW_PATCH = "Can not set HttpURLConnection.methods field to allow PATCH.";

private static final String METHOD_DELETE = "DELETE";
private static final String METHOD_GET = "GET";
private static final String METHOD_PATCH = "PATCH";
private static final String METHOD_POST = "POST";

private static final List<String> REQUEST_METHODS = Arrays.asList("GET", "PATCH", "POST", "DELETE");
private static final List<String> REQUEST_METHODS_WITH_PAYLOAD = Arrays.asList("PATCH", "POST");
private static final List<String> REQUEST_METHODS = Arrays.asList(METHOD_DELETE, METHOD_GET, METHOD_PATCH, METHOD_POST);
private static final List<String> REQUEST_METHODS_WITH_PAYLOAD = Arrays.asList(METHOD_PATCH, METHOD_POST);
private static final List<String> PROTOCOLS = Arrays.asList(new String[]{"http://", "https://"});

// Used when the actual version can not be parsed.
private static final double DEFAULT_JAVA_VERSION = 0.0;

// Indicates whether we've overridden HttpURLConnection's behaviour to
// allow PATCH requests yet. Also see docs on allowPatchRequestsIfNeeded().
private static boolean isPatchRequestAllowed = false;

private final String accessKey;
private final String serviceUrl;
private final String clientVersion = "2.0.0";
Expand Down Expand Up @@ -191,6 +204,16 @@ protected <P> APIResponse doRequest(final String method, final String url, final
HttpURLConnection connection = null;
InputStream inputStream = null;

if (METHOD_PATCH.equalsIgnoreCase(method)) {
// It'd perhaps be cleaner to call this in the constructor, but
// we'd then need to throw GeneralExceptions from there. This means
// it wouldn't be possible to declare AND initialize _instance_
// fields of MessageBirdServiceImpl at the same time. This method
// already throws this exception, so now we don't have to pollute
// our public API further.
allowPatchRequestsIfNeeded();
}

try {
connection = getConnection(url, payload, method);
int status = connection.getResponseCode();
Expand All @@ -213,6 +236,60 @@ protected <P> APIResponse doRequest(final String method, final String url, final
}
}

/**
* By default, HttpURLConnection does not support PATCH requests. We can
* however work around this with reflection. Many thanks to okutane on
* StackOverflow: https://stackoverflow.com/a/46323891/3521243.
*/
private synchronized static void allowPatchRequestsIfNeeded() throws GeneralException {
if (isPatchRequestAllowed) {
// Don't do anything if we've run this method before. We're in a
// synchronized block, so return ASAP.
return;
}

try {
// Ensure we can access the fields we need to set.
Field methodsField = HttpURLConnection.class.getDeclaredField("methods");
methodsField.setAccessible(true);

Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(methodsField, methodsField.getModifiers() & ~Modifier.FINAL);

Object noInstanceBecauseStaticField = null;

// Determine what methods should be allowed.
String[] existingMethods = (String[]) methodsField.get(noInstanceBecauseStaticField);
String[] allowedMethods = getAllowedMethods(existingMethods);

// Override the actual field to allow PATCH.
methodsField.set(noInstanceBecauseStaticField, allowedMethods);

// Set flag so we only have to run this once.
isPatchRequestAllowed = true;
} catch (IllegalAccessException | NoSuchFieldException e) {
throw new GeneralException(CAN_NOT_ALLOW_PATCH);
}
}

/**
* Appends PATCH to the provided array.
*
* @param existingMethods Methods that are, and must be, allowed.
* @return New array also containing PATCH.
*/
private static String[] getAllowedMethods(String[] existingMethods) {
int listCapacity = existingMethods.length + 1;

List<String> allowedMethods = new ArrayList<>(listCapacity);

allowedMethods.addAll(Arrays.asList(existingMethods));
allowedMethods.add(METHOD_PATCH);

return allowedMethods.toArray(new String[0]);
}

/**
* Reads the stream until it has no more bytes and returns a UTF-8 encoded
* string representation.
Expand Down Expand Up @@ -277,14 +354,7 @@ public <P> HttpURLConnection getConnection(final String serviceUrl, final P post
connection.setRequestProperty("User-agent", userAgentString);

if ("POST".equals(requestType) || "PATCH".equals(requestType)) {
if ("PATCH".equals(requestType)) {
// HttpURLConnection does not support PATCH so we'll send a
// POST, but instruct the server to interpret it as a PATCH.
// See: https://stackoverflow.com/a/32503192/3521243
connection.setRequestProperty("X-HTTP-Method-Override", "PATCH");
}

connection.setRequestMethod("POST");
connection.setRequestMethod(requestType);
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/json");
ObjectMapper mapper = new ObjectMapper();
Expand Down Expand Up @@ -448,6 +518,4 @@ private String getPathVariables(final Map<String, Object> map) {
}
return bpath.toString();
}


}
}

0 comments on commit 8bb69dc

Please sign in to comment.