Refactored classes in thematically packages
authorKai Moritz <kai@juplo.de>
Thu, 8 Oct 2015 07:12:49 +0000 (09:12 +0200)
committerKai Moritz <kai@juplo.de>
Sat, 7 Nov 2015 18:26:04 +0000 (19:26 +0100)
31 files changed:
src/main/java/de/juplo/facebook/FacebookUtils.java
src/main/java/de/juplo/facebook/GraphApiErrorHandler.java [deleted file]
src/main/java/de/juplo/facebook/GraphApiException.java [deleted file]
src/main/java/de/juplo/facebook/GraphApiExceptionJackson1Deserializer.java [deleted file]
src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Deserializer.java [deleted file]
src/main/java/de/juplo/facebook/GraphMethodException.java [deleted file]
src/main/java/de/juplo/facebook/OAuthException.java [deleted file]
src/main/java/de/juplo/facebook/PageMigratedException.java [deleted file]
src/main/java/de/juplo/facebook/RateExceededException.java [deleted file]
src/main/java/de/juplo/facebook/SignedRequestAwareAuthorizationCodeAccessTokenProvider.java [deleted file]
src/main/java/de/juplo/facebook/SignedRequestAwareUserRedirectRequiredException.java [deleted file]
src/main/java/de/juplo/facebook/UnexpectedErrorException.java [deleted file]
src/main/java/de/juplo/facebook/UnknownErrorException.java [deleted file]
src/main/java/de/juplo/facebook/UnsupportedGetRequestException.java [deleted file]
src/main/java/de/juplo/facebook/client/GraphApiErrorHandler.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/exceptions/GraphApiException.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/exceptions/GraphApiExceptionJackson1Deserializer.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/exceptions/GraphApiExceptionJackson2Deserializer.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/exceptions/GraphMethodException.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/exceptions/OAuthException.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/exceptions/PageMigratedException.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/exceptions/RateExceededException.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/exceptions/SignedRequestAwareUserRedirectRequiredException.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/exceptions/UnexpectedErrorException.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/exceptions/UnknownErrorException.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/exceptions/UnsupportedGetRequestException.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/token/SignedRequestAwareAuthorizationCodeAccessTokenProvider.java [new file with mode: 0644]
src/test/java/de/juplo/facebook/GraphApiErrorHandlerTest.java [deleted file]
src/test/java/de/juplo/facebook/MockClientHttpRequestFactory.java [deleted file]
src/test/java/de/juplo/facebook/client/GraphApiErrorHandlerTest.java [new file with mode: 0644]
src/test/java/de/juplo/facebook/client/MockClientHttpRequestFactory.java [new file with mode: 0644]

index a0e6738..b2b8261 100644 (file)
@@ -1,6 +1,8 @@
 package de.juplo.facebook;
 
 
+import de.juplo.facebook.token.SignedRequestAwareAuthorizationCodeAccessTokenProvider;
+import de.juplo.facebook.client.GraphApiErrorHandler;
 import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
diff --git a/src/main/java/de/juplo/facebook/GraphApiErrorHandler.java b/src/main/java/de/juplo/facebook/GraphApiErrorHandler.java
deleted file mode 100644 (file)
index 2864843..0000000
+++ /dev/null
@@ -1,147 +0,0 @@
-package de.juplo.facebook;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.List;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.client.ClientHttpResponse;
-import org.springframework.http.converter.HttpMessageConverter;
-import org.springframework.http.converter.HttpMessageNotReadableException;
-import org.springframework.security.oauth2.client.http.OAuth2ErrorHandler;
-import org.springframework.util.FileCopyUtils;
-import org.springframework.web.client.HttpMessageConverterExtractor;
-import org.springframework.web.client.RestClientException;
-import org.springframework.web.client.RestTemplate;
-
-
-
-/**
- *
- * @author kai
- */
-public class GraphApiErrorHandler extends OAuth2ErrorHandler
-{
-  private final OAuth2ErrorHandler errorHandler;
-  private List<HttpMessageConverter<?>> messageConverters =
-      new RestTemplate().getMessageConverters();
-
-
-  public GraphApiErrorHandler(OAuth2ErrorHandler errorHandler)
-  {
-    super(null);
-    this.errorHandler = errorHandler;
-  }
-
-
-       /**
-        * @param messageConverters the messageConverters to set
-        */
-  @Override
-  public void setMessageConverters(
-      List<HttpMessageConverter<?>> messageConverters
-      )
-  {
-    this.messageConverters = messageConverters;
-    errorHandler.setMessageConverters(messageConverters);
-  }
-
-  @Override
-  public boolean hasError(ClientHttpResponse response) throws IOException
-  {
-    return
-        HttpStatus.Series.CLIENT_ERROR.equals(response.getStatusCode().series())
-        || this.errorHandler.hasError(response);
-  }
-
-  @Override
-  public void handleError(final ClientHttpResponse response) throws IOException
-  {
-    if (!HttpStatus.Series.CLIENT_ERROR.equals(response.getStatusCode().series()))
-    {
-      // We should only care about 400 level errors. Ex: A 500 server error shouldn't
-      // be an oauth related error.
-      errorHandler.handleError(response);
-    }
-    else
-    {
-      // Need to use buffered response because input stream may need to be consumed multiple times.
-      ClientHttpResponse bufferedResponse = new ClientHttpResponse()
-      {
-        private byte[] lazyBody;
-
-        @Override
-        public HttpStatus getStatusCode() throws IOException
-        {
-          return response.getStatusCode();
-        }
-
-        @Override
-        public synchronized InputStream getBody() throws IOException
-        {
-          if (lazyBody == null) {
-            InputStream bodyStream = response.getBody();
-            if (bodyStream != null) {
-              lazyBody = FileCopyUtils.copyToByteArray(bodyStream);
-            }
-            else {
-              lazyBody = new byte[0];
-            }
-          }
-          return new ByteArrayInputStream(lazyBody);
-        }
-
-        @Override
-        public HttpHeaders getHeaders()
-        {
-          return response.getHeaders();
-        }
-
-        @Override
-        public String getStatusText() throws IOException
-        {
-          return response.getStatusText();
-        }
-
-        @Override
-        public void close()
-        {
-          response.close();
-        }
-
-        @Override
-        public int getRawStatusCode() throws IOException
-        {
-          return response.getRawStatusCode();
-        }
-      };
-
-
-      HttpMessageConverterExtractor<GraphApiException> extractor =
-          new HttpMessageConverterExtractor<>(
-              GraphApiException.class,
-              messageConverters
-              );
-
-      try
-      {
-        GraphApiException body = extractor.extractData(bufferedResponse);
-        if (body != null)
-        {
-          // If we can get an OAuth2Exception already from the body, it is likely
-          // to have more information than the header does, so just re-throw it
-          // here.
-          body.setHttpErrorCode(response.getRawStatusCode());
-          throw body;
-        }
-      }
-      catch (RestClientException|HttpMessageNotReadableException e)
-      {
-        // ignore
-      }
-
-      errorHandler.handleError(bufferedResponse);
-    }
-  }
-}
diff --git a/src/main/java/de/juplo/facebook/GraphApiException.java b/src/main/java/de/juplo/facebook/GraphApiException.java
deleted file mode 100644 (file)
index 18eaf81..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-package de.juplo.facebook;
-
-import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
-
-/**
- * Base exception for Facebook Graph-Api exceptions.
- * 
- * @author Kai Moritz
- */
-@org.codehaus.jackson.map.annotate.JsonDeserialize(using = GraphApiExceptionJackson1Deserializer.class)
-@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = GraphApiExceptionJackson2Deserializer.class)
-public class GraphApiException extends OAuth2Exception
-{
-  private final String type;
-  private final int code;
-
-  private int httpErrorCode;
-
-
-  public GraphApiException(String message, String type, int code)
-  {
-    super(message);
-    this.type = type;
-    this.code = code;
-  }
-
-
-  public String getType()
-  {
-    return type;
-  }
-
-  public int getCode()
-  {
-    return code;
-  }
-
-  @Override
-  public int getHttpErrorCode()
-  {
-    return httpErrorCode == 0 ? super.getHttpErrorCode() : httpErrorCode;
-  }
-
-  public void setHttpErrorCode(int httpErrorCode)
-  {
-    this.httpErrorCode = httpErrorCode;
-  }
-
-  @Override
-  public String toString()
-  {
-    StringBuilder builder = new StringBuilder();
-    builder.append("{error:{\"message\":\"");
-    builder.append(getMessage().replaceAll("\"", "\\\""));
-    builder.append("\",\"type\":");
-    builder.append(type.replaceAll("\"", "\\\""));
-    builder.append("\",\"code\":");
-    builder.append(code);
-    builder.append("}}");
-    return builder.toString();
-  }
-}
diff --git a/src/main/java/de/juplo/facebook/GraphApiExceptionJackson1Deserializer.java b/src/main/java/de/juplo/facebook/GraphApiExceptionJackson1Deserializer.java
deleted file mode 100644 (file)
index 8a1d3bf..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-package de.juplo.facebook;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import org.codehaus.jackson.JsonParser;
-import org.codehaus.jackson.JsonProcessingException;
-import org.codehaus.jackson.JsonToken;
-import org.codehaus.jackson.map.DeserializationContext;
-import org.codehaus.jackson.map.JsonDeserializer;
-
-/**
- * @author Kai Moritz
- */
-public class GraphApiExceptionJackson1Deserializer
-    extends
-      JsonDeserializer<GraphApiException>
-{
-
-  @Override
-  public GraphApiException deserialize(
-      JsonParser jp,
-      DeserializationContext ctxt
-      )
-      throws
-        IOException,
-        JsonProcessingException
-  {
-    JsonToken t = jp.getCurrentToken();
-    if (t != JsonToken.START_OBJECT)
-      return null;
-
-    t = jp.nextToken();
-    if (t != JsonToken.FIELD_NAME)
-      return null;
-
-    if (!jp.getCurrentName().equals("error"))
-      return null;
-
-    t = jp.nextToken();
-    if (t != JsonToken.START_OBJECT)
-      return null;
-  
-    String message = null, type = null;
-    Integer code = null;
-
-    t = jp.nextToken();
-    Map<String, String> map = new HashMap<>();
-    for (; t == JsonToken.FIELD_NAME; t = jp.nextToken())
-    {
-      // Must point to field name
-      String fieldName = jp.getCurrentName();
-      // And then the value...
-      t = jp.nextToken();
-
-      switch (t)
-      {
-        case VALUE_STRING:
-          switch(fieldName.toLowerCase())
-          {
-            case "message":
-              message = jp.getText();
-              break;
-            case "type":
-              type = jp.getText();
-              break;
-            default:
-              return null;
-          }
-          break;
-        case VALUE_NUMBER_INT:
-          if (!fieldName.equalsIgnoreCase("code"))
-            return null;
-          code = jp.getValueAsInt();
-          break;
-        default:
-          return null;
-      }
-    }
-
-    if (message == null || type == null || code == null)
-      return null;
-
-    switch (code)
-    {
-      case 1:   return new UnknownErrorException();
-      case 2:   return new UnexpectedErrorException();
-      case 21:  return new PageMigratedException(message);
-      case 100: return new UnsupportedGetRequestException();
-      case 613: return new RateExceededException();
-      default:  return new GraphApiException(message, type, code);
-    }
-  }
-}
diff --git a/src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Deserializer.java b/src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Deserializer.java
deleted file mode 100644 (file)
index becb51f..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-package de.juplo.facebook;
-
-
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.JsonToken;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-
-
-/**
- * @author Kai Moritz
- *
- */
-public class GraphApiExceptionJackson2Deserializer
-    extends
-      StdDeserializer<GraphApiException>
-{
-  public GraphApiExceptionJackson2Deserializer()
-  {
-    super(GraphApiException.class);
-  }
-
-  @Override
-  public GraphApiException deserialize(
-      JsonParser jp,
-      DeserializationContext ctxt
-      )
-      throws
-        IOException,
-        JsonProcessingException
-  {
-    JsonToken t = jp.getCurrentToken();
-    if (t != JsonToken.START_OBJECT)
-      return null;
-
-    t = jp.nextToken();
-    if (t != JsonToken.FIELD_NAME)
-      return null;
-
-    if (!jp.getCurrentName().equals("error"))
-      return null;
-
-    t = jp.nextToken();
-    if (t != JsonToken.START_OBJECT)
-      return null;
-  
-    String message = null, type = null;
-    Integer code = null;
-
-    t = jp.nextToken();
-    Map<String, String> map = new HashMap<>();
-    for (; t == JsonToken.FIELD_NAME; t = jp.nextToken())
-    {
-      // Must point to field name
-      String fieldName = jp.getCurrentName();
-      // And then the value...
-      t = jp.nextToken();
-
-      switch (t)
-      {
-        case VALUE_STRING:
-          switch(fieldName.toLowerCase())
-          {
-            case "message":
-              message = jp.getText();
-              break;
-            case "type":
-              type = jp.getText();
-              break;
-            default:
-              return null;
-          }
-          break;
-        case VALUE_NUMBER_INT:
-          if (!fieldName.equalsIgnoreCase("code"))
-            return null;
-          code = jp.getValueAsInt();
-          break;
-        default:
-          return null;
-      }
-    }
-
-    if (message == null || type == null || code == null)
-      return null;
-
-    switch (code)
-    {
-      case 1:   return new UnknownErrorException();
-      case 2:   return new UnexpectedErrorException();
-      case 21:  return new PageMigratedException(message);
-      case 100: return new UnsupportedGetRequestException();
-      case 613: return new RateExceededException();
-      default:  return new GraphApiException(message, type, code);
-    }
-  }
-}
diff --git a/src/main/java/de/juplo/facebook/GraphMethodException.java b/src/main/java/de/juplo/facebook/GraphMethodException.java
deleted file mode 100644 (file)
index 934243c..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.juplo.facebook;
-
-
-/**
- *
- * @author kai
- */
-public abstract class GraphMethodException extends GraphApiException
-{
-  public GraphMethodException(String message, int code)
-  {
-    super(message, "GraphMethodException", code);
-  }
-}
diff --git a/src/main/java/de/juplo/facebook/OAuthException.java b/src/main/java/de/juplo/facebook/OAuthException.java
deleted file mode 100644 (file)
index 1e80fe3..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.juplo.facebook;
-
-
-/**
- *
- * @author kai
- */
-public abstract class OAuthException extends GraphApiException
-{
-  public OAuthException(String message, int code)
-  {
-    super(message, "OAuthException", code);
-  }
-}
diff --git a/src/main/java/de/juplo/facebook/PageMigratedException.java b/src/main/java/de/juplo/facebook/PageMigratedException.java
deleted file mode 100644 (file)
index c3da159..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-package de.juplo.facebook;
-
-
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-
-
-/**
- *
- * @author kai
- */
-public class PageMigratedException extends OAuthException
-{
-  private final static Pattern pattern =
-      Pattern.compile("Page ID ([0-9]+) was migrated to page ID ([0-9]+)");
-
-  private final Long oldId, newId;
-
-
-  public PageMigratedException(String message)
-  {
-    super(message, 21);
-    Matcher matcher = pattern.matcher(message);
-    if (!matcher.find())
-      throw new RuntimeException("Could not parse migration-error: " + message);
-    oldId = Long.parseLong(matcher.group(1));
-    newId = Long.parseLong(matcher.group(2));
-  }
-
-
-  public Long getOldId()
-  {
-    return oldId;
-  }
-
-  public Long getNewId()
-  {
-    return newId;
-  }
-}
diff --git a/src/main/java/de/juplo/facebook/RateExceededException.java b/src/main/java/de/juplo/facebook/RateExceededException.java
deleted file mode 100644 (file)
index b633069..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.juplo.facebook;
-
-
-/**
- *
- * @author kai
- */
-public class RateExceededException extends OAuthException
-{
-  public RateExceededException()
-  {
-    super("(#613) Calls to stream have exceeded the rate of 600 calls per 600 seconds.", 613);
-  }
-}
diff --git a/src/main/java/de/juplo/facebook/SignedRequestAwareAuthorizationCodeAccessTokenProvider.java b/src/main/java/de/juplo/facebook/SignedRequestAwareAuthorizationCodeAccessTokenProvider.java
deleted file mode 100644 (file)
index fcae74a..0000000
+++ /dev/null
@@ -1,248 +0,0 @@
-package de.juplo.facebook;
-
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-import org.apache.commons.codec.binary.Base64;
-import org.codehaus.jackson.JsonNode;
-import org.codehaus.jackson.map.ObjectMapper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
-import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
-import org.springframework.security.oauth2.client.token.AccessTokenRequest;
-import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
-import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
-import org.springframework.security.oauth2.common.OAuth2AccessToken;
-
-
-/**
- * This class extends {@link AuthorizationCodeAccessTokenProvider} and adds
- * support for signed requests, which are issued by Facebook, if the Canvas-
- * or Tab-Page of a Facebook-App is accessed for the first time.
- *
- * @author Kai Moritz
- */
-public class SignedRequestAwareAuthorizationCodeAccessTokenProvider
-    extends AuthorizationCodeAccessTokenProvider
-{
-  private final Logger log =
-      LoggerFactory.getLogger(SignedRequestAwareAuthorizationCodeAccessTokenProvider.class);
-  private final static Pattern pattern =
-      Pattern.compile("([a-zA-Z0-9_-]+)\\.([a-zA-Z0-9_-]+)");
-
-  public final static String PARAM_SIGNED_REQUEST = "signed_request";
-
-
-  private String secret;
-  private ObjectMapper objectMapper;
-
-
-  @Override
-  public OAuth2AccessToken obtainAccessToken(
-      OAuth2ProtectedResourceDetails details,
-      AccessTokenRequest parameters
-      )
-  {
-    try
-    {
-      return super.obtainAccessToken(details, parameters);
-    }
-    catch (UserRedirectRequiredException redirect)
-    {
-      log.debug("no valid access-token available: checking for signed request");
-
-      if (!parameters.containsKey(PARAM_SIGNED_REQUEST))
-      {
-        log.info(
-            "parameter " + PARAM_SIGNED_REQUEST + " is not present"
-            );
-        throw redirect;
-      }
-
-      String signed_request = parameters.get(PARAM_SIGNED_REQUEST).get(0);
-
-      Matcher matcher = pattern.matcher(signed_request);
-      if (!matcher.matches())
-      {
-        log.error("invalid signed_request: {}", signed_request);
-        throw redirect;
-      }
-
-      String signature = matcher.group(1);
-      String rawdata = matcher.group(2);
-
-      String data;
-      try
-      {
-        data = new String(Base64.decodeBase64(rawdata), "UTF-8");
-        log.debug("JSON-data: {}", data);
-      }
-      catch (UnsupportedEncodingException e)
-      {
-        log.error("error while decoding data: {}", e.getMessage());
-        throw redirect;
-      }
-
-      JsonNode json;
-      try
-      {
-        json = objectMapper.readTree(data);
-      }
-      catch (IOException e)
-      {
-        log.error("error \"{}\" while parsing JSON-data: {}", e, data);
-        throw redirect;
-      }
-
-      String algorithm = "";
-      try
-      {
-        algorithm = json.get("algorithm").asText();
-      }
-      catch (NullPointerException e) {}
-      if (algorithm.isEmpty())
-      {
-        log.error("field \"algorithm\" is missing: {}", data);
-        throw redirect;
-      }
-      algorithm = algorithm.replaceAll("-", "");
-
-      String check;
-      try
-      {
-        SecretKeySpec key = new SecretKeySpec(secret.getBytes("UTF-8"), algorithm);
-        Mac mac = Mac.getInstance(algorithm);
-        mac.init(key);
-        byte[] hmacData = mac.doFinal(rawdata.getBytes("UTF-8"));
-        check = new String(Base64.encodeBase64URLSafe(hmacData), "UTF-8");
-      }
-      catch (
-          UnsupportedEncodingException |
-          NoSuchAlgorithmException |
-          InvalidKeyException |
-          IllegalStateException e
-          )
-      {
-        log.error("signature check failed!", e);
-        throw redirect;
-      }
-      if (!check.equals(signature))
-      {
-        log.error("signature does not match!");
-        throw redirect;
-      }
-
-      /**
-       * Extract additional information and store it in the token
-       * See:
-       * https://developers.facebook.com/docs/reference/login/signed-request/
-       * TODO:
-       * - Attribute "code"
-       */
-      Map<String,Object> additionalInformation = new HashMap<>();
-      try
-      {
-        additionalInformation.put(
-            "issued_at",
-            new Date(json.get("issued_at").getLongValue()*1000L)
-            );
-        Map<String,Object> user = new HashMap<>();
-        user.put(
-            "country",
-            json.get("user").get("country").asText()
-            );
-        user.put(
-            "locale",
-            json.get("user").get("locale").asText()
-            );
-        user.put(
-            "age_min",
-            json.get("user").get("age").get("min").getNumberValue()
-            );
-        if (json.get("user") != null && json.get("user").get("max") != null)
-          user.put(
-              "age_max",
-              json.get("user").get("age").get("max").getNumberValue()
-              );
-        additionalInformation.put("user", user);
-        if (json.get("app_data") != null)
-          additionalInformation.put("app_data", json.get("app_data").asText());
-        if (json.get("page") != null)
-        {
-          Map<String,Object> page = new HashMap<>();
-          page.put("id", json.get("page").get("id").asText());
-          page.put("liked", json.get("page").get("liked").asBoolean());
-          page.put("admin", json.get("page").get("admin").asBoolean());
-          additionalInformation.put("page", page);
-        }
-      }
-      catch (NullPointerException e)
-      {
-        log.warn("expected additional data is missing: {}", data);
-      }
-
-      DefaultOAuth2AccessToken token = null;
-      try
-      {
-        String value = json.get("oauth_token").asText();
-        if (value.isEmpty())
-        {
-          log.error("field \"oauth_token\" is missing: {}", data);
-          throw redirect;
-        }
-        token = new DefaultOAuth2AccessToken(value);
-        token.setExpiration(new Date(json.get("expires").getLongValue()*1000L));
-
-        additionalInformation.put(
-            "user_id",
-            json.get("user_id").asText()
-            );
-
-        token.setAdditionalInformation(additionalInformation);
-      }
-      catch (NullPointerException e)
-      {
-        if (token == null)
-        {
-          log.error("field \"oauth_token\" is missing: {}", data);
-          throw redirect;
-        }
-        else
-          log.warn("expected additional data is missing: {}", data);
-      }
-
-      return token;
-    }
-  }
-
-
-  public String getSecret()
-  {
-    return secret;
-  }
-
-  public void setSecret(String secret)
-  {
-    this.secret = secret;
-  }
-
-  public ObjectMapper getObjectMapper()
-  {
-    return objectMapper;
-  }
-
-  public void setObjectMapper(ObjectMapper objectMapper)
-  {
-    this.objectMapper = objectMapper;
-  }
-}
diff --git a/src/main/java/de/juplo/facebook/SignedRequestAwareUserRedirectRequiredException.java b/src/main/java/de/juplo/facebook/SignedRequestAwareUserRedirectRequiredException.java
deleted file mode 100644 (file)
index 7bad046..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-package de.juplo.facebook;
-
-
-import java.util.Map;
-import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
-
-
-
-/**
- *
- * @author kai
- */
-public class SignedRequestAwareUserRedirectRequiredException
-    extends
-      UserRedirectRequiredException
-{
-  private final Map<String,Object> signedRequestData;
-
-
-  public SignedRequestAwareUserRedirectRequiredException(
-      String redirectUri,
-      Map<String,String> requestParams,
-      Map<String,Object> signedRequestData
-      )
-  {
-    super(redirectUri, requestParams);
-    this.signedRequestData = signedRequestData;
-  }
-
-
-  public Map<String, Object> getSignedRequestData()
-  {
-    return signedRequestData;
-  }
-}
diff --git a/src/main/java/de/juplo/facebook/UnexpectedErrorException.java b/src/main/java/de/juplo/facebook/UnexpectedErrorException.java
deleted file mode 100644 (file)
index 031195f..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.juplo.facebook;
-
-
-/**
- *
- * @author kai
- */
-public class UnexpectedErrorException extends OAuthException
-{
-  public UnexpectedErrorException()
-  {
-    super("An unexpected error has occurred. Please retry your request later.", 2);
-  }
-}
diff --git a/src/main/java/de/juplo/facebook/UnknownErrorException.java b/src/main/java/de/juplo/facebook/UnknownErrorException.java
deleted file mode 100644 (file)
index 9790973..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.juplo.facebook;
-
-
-/**
- *
- * @author kai
- */
-public class UnknownErrorException extends OAuthException
-{
-  public UnknownErrorException()
-  {
-    super("An unknown error has occurred.", 1);
-  }
-}
diff --git a/src/main/java/de/juplo/facebook/UnsupportedGetRequestException.java b/src/main/java/de/juplo/facebook/UnsupportedGetRequestException.java
deleted file mode 100644 (file)
index f1020c1..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.juplo.facebook;
-
-
-/**
- *
- * @author kai
- */
-public class UnsupportedGetRequestException extends GraphMethodException
-{
-  public UnsupportedGetRequestException()
-  {
-    super("Unsupported get request.", 100);
-  }
-}
diff --git a/src/main/java/de/juplo/facebook/client/GraphApiErrorHandler.java b/src/main/java/de/juplo/facebook/client/GraphApiErrorHandler.java
new file mode 100644 (file)
index 0000000..faff55e
--- /dev/null
@@ -0,0 +1,148 @@
+package de.juplo.facebook.client;
+
+import de.juplo.facebook.exceptions.GraphApiException;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.security.oauth2.client.http.OAuth2ErrorHandler;
+import org.springframework.util.FileCopyUtils;
+import org.springframework.web.client.HttpMessageConverterExtractor;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+
+
+
+/**
+ *
+ * @author kai
+ */
+public class GraphApiErrorHandler extends OAuth2ErrorHandler
+{
+  private final OAuth2ErrorHandler errorHandler;
+  private List<HttpMessageConverter<?>> messageConverters =
+      new RestTemplate().getMessageConverters();
+
+
+  public GraphApiErrorHandler(OAuth2ErrorHandler errorHandler)
+  {
+    super(null);
+    this.errorHandler = errorHandler;
+  }
+
+
+       /**
+        * @param messageConverters the messageConverters to set
+        */
+  @Override
+  public void setMessageConverters(
+      List<HttpMessageConverter<?>> messageConverters
+      )
+  {
+    this.messageConverters = messageConverters;
+    errorHandler.setMessageConverters(messageConverters);
+  }
+
+  @Override
+  public boolean hasError(ClientHttpResponse response) throws IOException
+  {
+    return
+        HttpStatus.Series.CLIENT_ERROR.equals(response.getStatusCode().series())
+        || this.errorHandler.hasError(response);
+  }
+
+  @Override
+  public void handleError(final ClientHttpResponse response) throws IOException
+  {
+    if (!HttpStatus.Series.CLIENT_ERROR.equals(response.getStatusCode().series()))
+    {
+      // We should only care about 400 level errors. Ex: A 500 server error shouldn't
+      // be an oauth related error.
+      errorHandler.handleError(response);
+    }
+    else
+    {
+      // Need to use buffered response because input stream may need to be consumed multiple times.
+      ClientHttpResponse bufferedResponse = new ClientHttpResponse()
+      {
+        private byte[] lazyBody;
+
+        @Override
+        public HttpStatus getStatusCode() throws IOException
+        {
+          return response.getStatusCode();
+        }
+
+        @Override
+        public synchronized InputStream getBody() throws IOException
+        {
+          if (lazyBody == null) {
+            InputStream bodyStream = response.getBody();
+            if (bodyStream != null) {
+              lazyBody = FileCopyUtils.copyToByteArray(bodyStream);
+            }
+            else {
+              lazyBody = new byte[0];
+            }
+          }
+          return new ByteArrayInputStream(lazyBody);
+        }
+
+        @Override
+        public HttpHeaders getHeaders()
+        {
+          return response.getHeaders();
+        }
+
+        @Override
+        public String getStatusText() throws IOException
+        {
+          return response.getStatusText();
+        }
+
+        @Override
+        public void close()
+        {
+          response.close();
+        }
+
+        @Override
+        public int getRawStatusCode() throws IOException
+        {
+          return response.getRawStatusCode();
+        }
+      };
+
+
+      HttpMessageConverterExtractor<GraphApiException> extractor =
+          new HttpMessageConverterExtractor<>(
+              GraphApiException.class,
+              messageConverters
+              );
+
+      try
+      {
+        GraphApiException body = extractor.extractData(bufferedResponse);
+        if (body != null)
+        {
+          // If we can get an OAuth2Exception already from the body, it is likely
+          // to have more information than the header does, so just re-throw it
+          // here.
+          body.setHttpErrorCode(response.getRawStatusCode());
+          throw body;
+        }
+      }
+      catch (RestClientException|HttpMessageNotReadableException e)
+      {
+        // ignore
+      }
+
+      errorHandler.handleError(bufferedResponse);
+    }
+  }
+}
diff --git a/src/main/java/de/juplo/facebook/exceptions/GraphApiException.java b/src/main/java/de/juplo/facebook/exceptions/GraphApiException.java
new file mode 100644 (file)
index 0000000..03aa627
--- /dev/null
@@ -0,0 +1,62 @@
+package de.juplo.facebook.exceptions;
+
+import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
+
+/**
+ * Base exception for Facebook Graph-Api exceptions.
+ * 
+ * @author Kai Moritz
+ */
+@org.codehaus.jackson.map.annotate.JsonDeserialize(using = GraphApiExceptionJackson1Deserializer.class)
+@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = GraphApiExceptionJackson2Deserializer.class)
+public class GraphApiException extends OAuth2Exception
+{
+  private final String type;
+  private final int code;
+
+  private int httpErrorCode;
+
+
+  public GraphApiException(String message, String type, int code)
+  {
+    super(message);
+    this.type = type;
+    this.code = code;
+  }
+
+
+  public String getType()
+  {
+    return type;
+  }
+
+  public int getCode()
+  {
+    return code;
+  }
+
+  @Override
+  public int getHttpErrorCode()
+  {
+    return httpErrorCode == 0 ? super.getHttpErrorCode() : httpErrorCode;
+  }
+
+  public void setHttpErrorCode(int httpErrorCode)
+  {
+    this.httpErrorCode = httpErrorCode;
+  }
+
+  @Override
+  public String toString()
+  {
+    StringBuilder builder = new StringBuilder();
+    builder.append("{error:{\"message\":\"");
+    builder.append(getMessage().replaceAll("\"", "\\\""));
+    builder.append("\",\"type\":");
+    builder.append(type.replaceAll("\"", "\\\""));
+    builder.append("\",\"code\":");
+    builder.append(code);
+    builder.append("}}");
+    return builder.toString();
+  }
+}
diff --git a/src/main/java/de/juplo/facebook/exceptions/GraphApiExceptionJackson1Deserializer.java b/src/main/java/de/juplo/facebook/exceptions/GraphApiExceptionJackson1Deserializer.java
new file mode 100644 (file)
index 0000000..165e7ce
--- /dev/null
@@ -0,0 +1,94 @@
+package de.juplo.facebook.exceptions;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import org.codehaus.jackson.JsonParser;
+import org.codehaus.jackson.JsonProcessingException;
+import org.codehaus.jackson.JsonToken;
+import org.codehaus.jackson.map.DeserializationContext;
+import org.codehaus.jackson.map.JsonDeserializer;
+
+/**
+ * @author Kai Moritz
+ */
+public class GraphApiExceptionJackson1Deserializer
+    extends
+      JsonDeserializer<GraphApiException>
+{
+
+  @Override
+  public GraphApiException deserialize(
+      JsonParser jp,
+      DeserializationContext ctxt
+      )
+      throws
+        IOException,
+        JsonProcessingException
+  {
+    JsonToken t = jp.getCurrentToken();
+    if (t != JsonToken.START_OBJECT)
+      return null;
+
+    t = jp.nextToken();
+    if (t != JsonToken.FIELD_NAME)
+      return null;
+
+    if (!jp.getCurrentName().equals("error"))
+      return null;
+
+    t = jp.nextToken();
+    if (t != JsonToken.START_OBJECT)
+      return null;
+  
+    String message = null, type = null;
+    Integer code = null;
+
+    t = jp.nextToken();
+    Map<String, String> map = new HashMap<>();
+    for (; t == JsonToken.FIELD_NAME; t = jp.nextToken())
+    {
+      // Must point to field name
+      String fieldName = jp.getCurrentName();
+      // And then the value...
+      t = jp.nextToken();
+
+      switch (t)
+      {
+        case VALUE_STRING:
+          switch(fieldName.toLowerCase())
+          {
+            case "message":
+              message = jp.getText();
+              break;
+            case "type":
+              type = jp.getText();
+              break;
+            default:
+              return null;
+          }
+          break;
+        case VALUE_NUMBER_INT:
+          if (!fieldName.equalsIgnoreCase("code"))
+            return null;
+          code = jp.getValueAsInt();
+          break;
+        default:
+          return null;
+      }
+    }
+
+    if (message == null || type == null || code == null)
+      return null;
+
+    switch (code)
+    {
+      case 1:   return new UnknownErrorException();
+      case 2:   return new UnexpectedErrorException();
+      case 21:  return new PageMigratedException(message);
+      case 100: return new UnsupportedGetRequestException();
+      case 613: return new RateExceededException();
+      default:  return new GraphApiException(message, type, code);
+    }
+  }
+}
diff --git a/src/main/java/de/juplo/facebook/exceptions/GraphApiExceptionJackson2Deserializer.java b/src/main/java/de/juplo/facebook/exceptions/GraphApiExceptionJackson2Deserializer.java
new file mode 100644 (file)
index 0000000..ea0e987
--- /dev/null
@@ -0,0 +1,101 @@
+package de.juplo.facebook.exceptions;
+
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * @author Kai Moritz
+ *
+ */
+public class GraphApiExceptionJackson2Deserializer
+    extends
+      StdDeserializer<GraphApiException>
+{
+  public GraphApiExceptionJackson2Deserializer()
+  {
+    super(GraphApiException.class);
+  }
+
+  @Override
+  public GraphApiException deserialize(
+      JsonParser jp,
+      DeserializationContext ctxt
+      )
+      throws
+        IOException,
+        JsonProcessingException
+  {
+    JsonToken t = jp.getCurrentToken();
+    if (t != JsonToken.START_OBJECT)
+      return null;
+
+    t = jp.nextToken();
+    if (t != JsonToken.FIELD_NAME)
+      return null;
+
+    if (!jp.getCurrentName().equals("error"))
+      return null;
+
+    t = jp.nextToken();
+    if (t != JsonToken.START_OBJECT)
+      return null;
+  
+    String message = null, type = null;
+    Integer code = null;
+
+    t = jp.nextToken();
+    Map<String, String> map = new HashMap<>();
+    for (; t == JsonToken.FIELD_NAME; t = jp.nextToken())
+    {
+      // Must point to field name
+      String fieldName = jp.getCurrentName();
+      // And then the value...
+      t = jp.nextToken();
+
+      switch (t)
+      {
+        case VALUE_STRING:
+          switch(fieldName.toLowerCase())
+          {
+            case "message":
+              message = jp.getText();
+              break;
+            case "type":
+              type = jp.getText();
+              break;
+            default:
+              return null;
+          }
+          break;
+        case VALUE_NUMBER_INT:
+          if (!fieldName.equalsIgnoreCase("code"))
+            return null;
+          code = jp.getValueAsInt();
+          break;
+        default:
+          return null;
+      }
+    }
+
+    if (message == null || type == null || code == null)
+      return null;
+
+    switch (code)
+    {
+      case 1:   return new UnknownErrorException();
+      case 2:   return new UnexpectedErrorException();
+      case 21:  return new PageMigratedException(message);
+      case 100: return new UnsupportedGetRequestException();
+      case 613: return new RateExceededException();
+      default:  return new GraphApiException(message, type, code);
+    }
+  }
+}
diff --git a/src/main/java/de/juplo/facebook/exceptions/GraphMethodException.java b/src/main/java/de/juplo/facebook/exceptions/GraphMethodException.java
new file mode 100644 (file)
index 0000000..21c56c5
--- /dev/null
@@ -0,0 +1,14 @@
+package de.juplo.facebook.exceptions;
+
+
+/**
+ *
+ * @author kai
+ */
+public abstract class GraphMethodException extends GraphApiException
+{
+  public GraphMethodException(String message, int code)
+  {
+    super(message, "GraphMethodException", code);
+  }
+}
diff --git a/src/main/java/de/juplo/facebook/exceptions/OAuthException.java b/src/main/java/de/juplo/facebook/exceptions/OAuthException.java
new file mode 100644 (file)
index 0000000..4537720
--- /dev/null
@@ -0,0 +1,14 @@
+package de.juplo.facebook.exceptions;
+
+
+/**
+ *
+ * @author kai
+ */
+public abstract class OAuthException extends GraphApiException
+{
+  public OAuthException(String message, int code)
+  {
+    super(message, "OAuthException", code);
+  }
+}
diff --git a/src/main/java/de/juplo/facebook/exceptions/PageMigratedException.java b/src/main/java/de/juplo/facebook/exceptions/PageMigratedException.java
new file mode 100644 (file)
index 0000000..d327b6d
--- /dev/null
@@ -0,0 +1,41 @@
+package de.juplo.facebook.exceptions;
+
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+
+/**
+ *
+ * @author kai
+ */
+public class PageMigratedException extends OAuthException
+{
+  private final static Pattern pattern =
+      Pattern.compile("Page ID ([0-9]+) was migrated to page ID ([0-9]+)");
+
+  private final Long oldId, newId;
+
+
+  public PageMigratedException(String message)
+  {
+    super(message, 21);
+    Matcher matcher = pattern.matcher(message);
+    if (!matcher.find())
+      throw new RuntimeException("Could not parse migration-error: " + message);
+    oldId = Long.parseLong(matcher.group(1));
+    newId = Long.parseLong(matcher.group(2));
+  }
+
+
+  public Long getOldId()
+  {
+    return oldId;
+  }
+
+  public Long getNewId()
+  {
+    return newId;
+  }
+}
diff --git a/src/main/java/de/juplo/facebook/exceptions/RateExceededException.java b/src/main/java/de/juplo/facebook/exceptions/RateExceededException.java
new file mode 100644 (file)
index 0000000..cdda586
--- /dev/null
@@ -0,0 +1,14 @@
+package de.juplo.facebook.exceptions;
+
+
+/**
+ *
+ * @author kai
+ */
+public class RateExceededException extends OAuthException
+{
+  public RateExceededException()
+  {
+    super("(#613) Calls to stream have exceeded the rate of 600 calls per 600 seconds.", 613);
+  }
+}
diff --git a/src/main/java/de/juplo/facebook/exceptions/SignedRequestAwareUserRedirectRequiredException.java b/src/main/java/de/juplo/facebook/exceptions/SignedRequestAwareUserRedirectRequiredException.java
new file mode 100644 (file)
index 0000000..51fbb12
--- /dev/null
@@ -0,0 +1,35 @@
+package de.juplo.facebook.exceptions;
+
+
+import java.util.Map;
+import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
+
+
+
+/**
+ *
+ * @author kai
+ */
+public class SignedRequestAwareUserRedirectRequiredException
+    extends
+      UserRedirectRequiredException
+{
+  private final Map<String,Object> signedRequestData;
+
+
+  public SignedRequestAwareUserRedirectRequiredException(
+      String redirectUri,
+      Map<String,String> requestParams,
+      Map<String,Object> signedRequestData
+      )
+  {
+    super(redirectUri, requestParams);
+    this.signedRequestData = signedRequestData;
+  }
+
+
+  public Map<String, Object> getSignedRequestData()
+  {
+    return signedRequestData;
+  }
+}
diff --git a/src/main/java/de/juplo/facebook/exceptions/UnexpectedErrorException.java b/src/main/java/de/juplo/facebook/exceptions/UnexpectedErrorException.java
new file mode 100644 (file)
index 0000000..5aefff6
--- /dev/null
@@ -0,0 +1,14 @@
+package de.juplo.facebook.exceptions;
+
+
+/**
+ *
+ * @author kai
+ */
+public class UnexpectedErrorException extends OAuthException
+{
+  public UnexpectedErrorException()
+  {
+    super("An unexpected error has occurred. Please retry your request later.", 2);
+  }
+}
diff --git a/src/main/java/de/juplo/facebook/exceptions/UnknownErrorException.java b/src/main/java/de/juplo/facebook/exceptions/UnknownErrorException.java
new file mode 100644 (file)
index 0000000..6e336f4
--- /dev/null
@@ -0,0 +1,14 @@
+package de.juplo.facebook.exceptions;
+
+
+/**
+ *
+ * @author kai
+ */
+public class UnknownErrorException extends OAuthException
+{
+  public UnknownErrorException()
+  {
+    super("An unknown error has occurred.", 1);
+  }
+}
diff --git a/src/main/java/de/juplo/facebook/exceptions/UnsupportedGetRequestException.java b/src/main/java/de/juplo/facebook/exceptions/UnsupportedGetRequestException.java
new file mode 100644 (file)
index 0000000..1d5b44d
--- /dev/null
@@ -0,0 +1,14 @@
+package de.juplo.facebook.exceptions;
+
+
+/**
+ *
+ * @author kai
+ */
+public class UnsupportedGetRequestException extends GraphMethodException
+{
+  public UnsupportedGetRequestException()
+  {
+    super("Unsupported get request.", 100);
+  }
+}
diff --git a/src/main/java/de/juplo/facebook/token/SignedRequestAwareAuthorizationCodeAccessTokenProvider.java b/src/main/java/de/juplo/facebook/token/SignedRequestAwareAuthorizationCodeAccessTokenProvider.java
new file mode 100644 (file)
index 0000000..e187542
--- /dev/null
@@ -0,0 +1,248 @@
+package de.juplo.facebook.token;
+
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import org.apache.commons.codec.binary.Base64;
+import org.codehaus.jackson.JsonNode;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
+import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
+import org.springframework.security.oauth2.client.token.AccessTokenRequest;
+import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
+import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
+import org.springframework.security.oauth2.common.OAuth2AccessToken;
+
+
+/**
+ * This class extends {@link AuthorizationCodeAccessTokenProvider} and adds
+ * support for signed requests, which are issued by Facebook, if the Canvas-
+ * or Tab-Page of a Facebook-App is accessed for the first time.
+ *
+ * @author Kai Moritz
+ */
+public class SignedRequestAwareAuthorizationCodeAccessTokenProvider
+    extends AuthorizationCodeAccessTokenProvider
+{
+  private final Logger log =
+      LoggerFactory.getLogger(SignedRequestAwareAuthorizationCodeAccessTokenProvider.class);
+  private final static Pattern pattern =
+      Pattern.compile("([a-zA-Z0-9_-]+)\\.([a-zA-Z0-9_-]+)");
+
+  public final static String PARAM_SIGNED_REQUEST = "signed_request";
+
+
+  private String secret;
+  private ObjectMapper objectMapper;
+
+
+  @Override
+  public OAuth2AccessToken obtainAccessToken(
+      OAuth2ProtectedResourceDetails details,
+      AccessTokenRequest parameters
+      )
+  {
+    try
+    {
+      return super.obtainAccessToken(details, parameters);
+    }
+    catch (UserRedirectRequiredException redirect)
+    {
+      log.debug("no valid access-token available: checking for signed request");
+
+      if (!parameters.containsKey(PARAM_SIGNED_REQUEST))
+      {
+        log.info(
+            "parameter " + PARAM_SIGNED_REQUEST + " is not present"
+            );
+        throw redirect;
+      }
+
+      String signed_request = parameters.get(PARAM_SIGNED_REQUEST).get(0);
+
+      Matcher matcher = pattern.matcher(signed_request);
+      if (!matcher.matches())
+      {
+        log.error("invalid signed_request: {}", signed_request);
+        throw redirect;
+      }
+
+      String signature = matcher.group(1);
+      String rawdata = matcher.group(2);
+
+      String data;
+      try
+      {
+        data = new String(Base64.decodeBase64(rawdata), "UTF-8");
+        log.debug("JSON-data: {}", data);
+      }
+      catch (UnsupportedEncodingException e)
+      {
+        log.error("error while decoding data: {}", e.getMessage());
+        throw redirect;
+      }
+
+      JsonNode json;
+      try
+      {
+        json = objectMapper.readTree(data);
+      }
+      catch (IOException e)
+      {
+        log.error("error \"{}\" while parsing JSON-data: {}", e, data);
+        throw redirect;
+      }
+
+      String algorithm = "";
+      try
+      {
+        algorithm = json.get("algorithm").asText();
+      }
+      catch (NullPointerException e) {}
+      if (algorithm.isEmpty())
+      {
+        log.error("field \"algorithm\" is missing: {}", data);
+        throw redirect;
+      }
+      algorithm = algorithm.replaceAll("-", "");
+
+      String check;
+      try
+      {
+        SecretKeySpec key = new SecretKeySpec(secret.getBytes("UTF-8"), algorithm);
+        Mac mac = Mac.getInstance(algorithm);
+        mac.init(key);
+        byte[] hmacData = mac.doFinal(rawdata.getBytes("UTF-8"));
+        check = new String(Base64.encodeBase64URLSafe(hmacData), "UTF-8");
+      }
+      catch (
+          UnsupportedEncodingException |
+          NoSuchAlgorithmException |
+          InvalidKeyException |
+          IllegalStateException e
+          )
+      {
+        log.error("signature check failed!", e);
+        throw redirect;
+      }
+      if (!check.equals(signature))
+      {
+        log.error("signature does not match!");
+        throw redirect;
+      }
+
+      /**
+       * Extract additional information and store it in the token
+       * See:
+       * https://developers.facebook.com/docs/reference/login/signed-request/
+       * TODO:
+       * - Attribute "code"
+       */
+      Map<String,Object> additionalInformation = new HashMap<>();
+      try
+      {
+        additionalInformation.put(
+            "issued_at",
+            new Date(json.get("issued_at").getLongValue()*1000L)
+            );
+        Map<String,Object> user = new HashMap<>();
+        user.put(
+            "country",
+            json.get("user").get("country").asText()
+            );
+        user.put(
+            "locale",
+            json.get("user").get("locale").asText()
+            );
+        user.put(
+            "age_min",
+            json.get("user").get("age").get("min").getNumberValue()
+            );
+        if (json.get("user") != null && json.get("user").get("max") != null)
+          user.put(
+              "age_max",
+              json.get("user").get("age").get("max").getNumberValue()
+              );
+        additionalInformation.put("user", user);
+        if (json.get("app_data") != null)
+          additionalInformation.put("app_data", json.get("app_data").asText());
+        if (json.get("page") != null)
+        {
+          Map<String,Object> page = new HashMap<>();
+          page.put("id", json.get("page").get("id").asText());
+          page.put("liked", json.get("page").get("liked").asBoolean());
+          page.put("admin", json.get("page").get("admin").asBoolean());
+          additionalInformation.put("page", page);
+        }
+      }
+      catch (NullPointerException e)
+      {
+        log.warn("expected additional data is missing: {}", data);
+      }
+
+      DefaultOAuth2AccessToken token = null;
+      try
+      {
+        String value = json.get("oauth_token").asText();
+        if (value.isEmpty())
+        {
+          log.error("field \"oauth_token\" is missing: {}", data);
+          throw redirect;
+        }
+        token = new DefaultOAuth2AccessToken(value);
+        token.setExpiration(new Date(json.get("expires").getLongValue()*1000L));
+
+        additionalInformation.put(
+            "user_id",
+            json.get("user_id").asText()
+            );
+
+        token.setAdditionalInformation(additionalInformation);
+      }
+      catch (NullPointerException e)
+      {
+        if (token == null)
+        {
+          log.error("field \"oauth_token\" is missing: {}", data);
+          throw redirect;
+        }
+        else
+          log.warn("expected additional data is missing: {}", data);
+      }
+
+      return token;
+    }
+  }
+
+
+  public String getSecret()
+  {
+    return secret;
+  }
+
+  public void setSecret(String secret)
+  {
+    this.secret = secret;
+  }
+
+  public ObjectMapper getObjectMapper()
+  {
+    return objectMapper;
+  }
+
+  public void setObjectMapper(ObjectMapper objectMapper)
+  {
+    this.objectMapper = objectMapper;
+  }
+}
diff --git a/src/test/java/de/juplo/facebook/GraphApiErrorHandlerTest.java b/src/test/java/de/juplo/facebook/GraphApiErrorHandlerTest.java
deleted file mode 100644 (file)
index ef849cf..0000000
+++ /dev/null
@@ -1,702 +0,0 @@
-package de.juplo.facebook;
-
-import java.util.Date;
-import java.util.Map;
-import java.util.Set;
-import javax.annotation.Resource;
-import static org.junit.Assert.*;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.http.HttpStatus;
-import org.springframework.security.access.AccessDeniedException;
-import org.springframework.security.oauth2.client.OAuth2RestTemplate;
-import org.springframework.security.oauth2.client.http.OAuth2ErrorHandler;
-import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
-import org.springframework.security.oauth2.client.resource.UserApprovalRequiredException;
-import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
-import org.springframework.security.oauth2.client.token.AccessTokenProvider;
-import org.springframework.security.oauth2.client.token.AccessTokenRequest;
-import org.springframework.security.oauth2.common.OAuth2AccessToken;
-import static org.springframework.security.oauth2.common.OAuth2AccessToken.OAUTH2_TYPE;
-import org.springframework.security.oauth2.common.OAuth2RefreshToken;
-import org.springframework.test.context.ContextConfiguration;
-import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
-import org.springframework.web.client.HttpClientErrorException;
-
-
-
-/**
- *
- * @author kai
- */
-@RunWith(SpringJUnit4ClassRunner.class)
-@ContextConfiguration(
-  locations = {
-    "classpath:/spring/test-facebook-error-handler.xml"
-    })
-public class GraphApiErrorHandlerTest
-{
-  private static final Logger log =
-      LoggerFactory.getLogger(GraphApiErrorHandlerTest.class);
-
-  @Resource
-  private OAuth2RestTemplate clientTemplate;
-
-  private MockClientHttpRequestFactory requestFactory;
-
-
-  @Test
-  public void testError1()
-  {
-    log.info("testError1");
-
-
-    requestFactory.setBody(
-        "{\n" +
-        "  \"error\":\n" +
-        "  {\n" +
-        "    \"message\": \"An unknown error has occurred.\",\n" +
-        "    \"type\": \"OAuthException\",\n" +
-        "    \"code\": 1\n" +
-        "  }\n" +
-        "}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(UnknownErrorException e)
-    {
-      log.debug("{}", e.toString());
-      assertEquals("invalid_request", e.getOAuth2ErrorCode());
-      assertEquals(1, e.getCode());
-      assertEquals("An unknown error has occurred.", e.getMessage());
-      assertEquals("OAuthException", e.getType());
-    }
-  }
-
-  @Test
-  public void testError2()
-  {
-    log.info("testError2");
-
-
-    requestFactory.setBody(
-        "{\n" +
-        "  \"error\":\n" +
-        "  {\n" +
-        "    \"message\": \"An unexpected error has occurred. Please retry your request later.\",\n" +
-        "    \"type\": \"OAuthException\",\n" +
-        "    \"code\": 2\n" +
-        "  }\n" +
-        "}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(UnexpectedErrorException e)
-    {
-      log.debug("{}", e.toString());
-      assertEquals("invalid_request", e.getOAuth2ErrorCode());
-      assertEquals(2, e.getCode());
-      assertEquals("An unexpected error has occurred. Please retry your request later.", e.getMessage());
-      assertEquals("OAuthException", e.getType());
-    }
-  }
-
-  @Test
-  public void testError21()
-  {
-    log.info("testError21");
-
-
-    requestFactory.setBody(
-        "{\n" +
-        "  \"error\":\n" +
-        "  {\n" +
-        "    \"message\": \"(#21) Page ID 590408587650316 was migrated to page ID 1421620791415603.  Please update your API calls to the new ID\",\n" +
-        "    \"type\": \"OAuthException\",\n" +
-        "    \"code\": 21\n" +
-        "  }\n" +
-        "}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(PageMigratedException e)
-    {
-      log.debug("{}", e.toString());
-      assertEquals("invalid_request", e.getOAuth2ErrorCode());
-      assertEquals(21, e.getCode());
-      assertEquals("(#21) Page ID 590408587650316 was migrated to page ID 1421620791415603.  Please update your API calls to the new ID", e.getMessage());
-      assertEquals("OAuthException", e.getType());
-    }
-  }
-
-  @Test
-  public void testError100()
-  {
-    log.info("testError100");
-
-
-    requestFactory.setBody(
-        "{\n" +
-        "  \"error\":\n" +
-        "  {\n" +
-        "    \"message\": \"Unsupported get request.\",\n" +
-        "    \"type\": \"GraphMethodException\",\n" +
-        "    \"code\": 100\n" +
-        "  }\n" +
-        "}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(UnsupportedGetRequestException e)
-    {
-      log.debug("{}", e.toString());
-      assertEquals("invalid_request", e.getOAuth2ErrorCode());
-      assertEquals(100, e.getCode());
-      assertEquals("Unsupported get request.", e.getMessage());
-      assertEquals("GraphMethodException", e.getType());
-    }
-  }
-
-  @Test
-  public void testError613()
-  {
-    log.info("testError613");
-
-
-    requestFactory.setBody(
-        "{\n" +
-        "  \"error\":\n" +
-        "  {\n" +
-        "    \"message\": \"(#613) Calls to stream have exceeded the rate of 600 calls per 600 seconds.\",\n" +
-        "    \"type\": \"OAuthException\",\n" +
-        "    \"code\": 613\n" +
-        "  }\n" +
-        "}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(RateExceededException e)
-    {
-      log.debug("{}", e.toString());
-      assertEquals("invalid_request", e.getOAuth2ErrorCode());
-      assertEquals(613, e.getCode());
-      assertEquals("(#613) Calls to stream have exceeded the rate of 600 calls per 600 seconds.", e.getMessage());
-      assertEquals("OAuthException", e.getType());
-    }
-  }
-
-  @Test
-  public void testUnmappedError()
-  {
-    log.info("testUnmappedError");
-
-
-    requestFactory.setBody(
-        "{\n" +
-        "  \"error\":\n" +
-        "  {\n" +
-        "    \"message\": \"This error does not exist.\",\n" +
-        "    \"type\": \"NonexistentException\",\n" +
-        "    \"code\": 999999999\n" +
-        "  }\n" +
-        "}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(GraphApiException e)
-    {
-      log.debug("{}", e.toString());
-      assertEquals("invalid_request", e.getOAuth2ErrorCode());
-      assertEquals(999999999, e.getCode());
-      assertEquals("This error does not exist.", e.getMessage());
-      assertEquals("NonexistentException", e.getType());
-    }
-  }
-
-  @Test
-  public void testInvlalidErrors()
-  {
-    log.info("testInvalidErrors");
-
-
-    requestFactory.setBody(
-        "{\n" +
-        "  \"error\":\n" +
-        "  {\n" +
-        "    \"message\": null,\n" +
-        "    \"type\": \"Whatever\",\n" +
-        "    \"code\": 999999999\n" +
-        "  }\n" +
-        "}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-
-
-    requestFactory.setBody(
-        "{\n" +
-        "  \"error\":\n" +
-        "  {\n" +
-        "    \"type\": \"Whatever\",\n" +
-        "    \"code\": 999999999\n" +
-        "  }\n" +
-        "}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-
-
-    requestFactory.setBody(
-        "{\n" +
-        "  \"error\":\n" +
-        "  {\n" +
-        "    \"message\": \"Not a Graph-Api-Exception.\",\n" +
-        "    \"type\": null,\n" +
-        "    \"code\": 999999999\n" +
-        "  }\n" +
-        "}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-
-
-    requestFactory.setBody(
-        "{\n" +
-        "  \"error\":\n" +
-        "  {\n" +
-        "    \"message\": \"Not a Graph-Api-Exception.\",\n" +
-        "    \"code\": 999999999\n" +
-        "  }\n" +
-        "}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-
-
-    requestFactory.setBody(
-        "{\n" +
-        "  \"error\":\n" +
-        "  {\n" +
-        "    \"message\": \"Not a Graph-Api-Exception.\",\n" +
-        "    \"type\": \"Whatever\",\n" +
-        "    \"code\": \"some string\"\n" +
-        "  }\n" +
-        "}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-
-
-    requestFactory.setBody(
-        "{\n" +
-        "  \"error\":\n" +
-        "  {\n" +
-        "    \"message\": \"Not a Graph-Api-Exception.\",\n" +
-        "    \"type\": \"Whatever\",\n" +
-        "    \"code\": 9.9\n" +
-        "  }\n" +
-        "}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-
-
-    requestFactory.setBody(
-        "{\n" +
-        "  \"error\":\n" +
-        "  {\n" +
-        "    \"message\": \"Not a Graph-Api-Exception.\",\n" +
-        "    \"type\": \"Whatever\",\n" +
-        "    \"code\": null\n" +
-        "  }\n" +
-        "}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-
-
-    requestFactory.setBody(
-        "{\n" +
-        "  \"error\":\n" +
-        "  {\n" +
-        "    \"message\": \"Not a Graph-Api-Exception.\",\n" +
-        "    \"type\": \"Whatever\"\n" +
-        "  }\n" +
-        "}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-
-
-    requestFactory.setBody("{\"error\":{\"message\":null}}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-
-
-    requestFactory.setBody("{\"error\":{\"type\":null}}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-
-
-    requestFactory.setBody("{\"error\":{\"code\":null}}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-
-
-    requestFactory.setBody("{\"error\":{}}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-
-
-    requestFactory.setBody("{\"error\":\"some message\"}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-
-
-    requestFactory.setBody("{\"error\":null}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-
-
-    requestFactory.setBody("{\"some filed\":\"some message\"}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-
-
-    requestFactory.setBody("{}");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-
-
-    requestFactory.setBody("");
-
-    try
-    {
-      clientTemplate.getForObject("ANY", SOME.class);
-      fail("The expected exception was not thrown");
-    }
-    catch(HttpClientErrorException e)
-    {
-      log.debug("{}", e.toString());
-    }
-    catch(Exception e)
-    {
-      fail("A wrong exception was thrown: " + e.toString());
-    }
-  }
-
-
-  @Before
-  public void setUp()
-  {
-    requestFactory = new MockClientHttpRequestFactory();
-    requestFactory.setStatus(HttpStatus.BAD_REQUEST);
-    requestFactory.addHeader("Content-Type", "application/json");
-    clientTemplate.setRequestFactory(requestFactory);
-
-    clientTemplate.setErrorHandler(
-        new GraphApiErrorHandler(
-            (OAuth2ErrorHandler)clientTemplate.getErrorHandler()
-            )
-        );
-
-    clientTemplate.setAccessTokenProvider(new AccessTokenProvider()
-    {
-      @Override
-      public OAuth2AccessToken obtainAccessToken(
-          OAuth2ProtectedResourceDetails details,
-          AccessTokenRequest parameters
-          )
-          throws
-            UserRedirectRequiredException,
-            UserApprovalRequiredException,
-            AccessDeniedException
-      {
-        return new OAuth2AccessToken() {
-
-          @Override
-          public Map<String, Object> getAdditionalInformation()
-          {
-            throw new UnsupportedOperationException("Not supported yet.");
-          }
-
-          @Override
-          public Set<String> getScope()
-          {
-            throw new UnsupportedOperationException("Not supported yet.");
-          }
-
-          @Override
-          public OAuth2RefreshToken getRefreshToken()
-          {
-            throw new UnsupportedOperationException("Not supported yet.");
-          }
-
-          @Override
-          public String getTokenType()
-          {
-            return OAUTH2_TYPE;
-          }
-
-          @Override
-          public boolean isExpired()
-          {
-            return false;
-          }
-
-          @Override
-          public Date getExpiration()
-          {
-            throw new UnsupportedOperationException("Not supported yet.");
-          }
-
-          @Override
-          public int getExpiresIn()
-          {
-            throw new UnsupportedOperationException("Not supported yet.");
-          }
-
-          @Override
-          public String getValue()
-          {
-            return "ANY";
-          }
-        };
-      }
-
-      @Override
-      public boolean supportsResource(OAuth2ProtectedResourceDetails resource)
-      {
-        return true;
-      }
-
-      @Override
-      public OAuth2AccessToken refreshAccessToken(
-          OAuth2ProtectedResourceDetails resource,
-          OAuth2RefreshToken refreshToken,
-          AccessTokenRequest request
-          )
-          throws
-            UserRedirectRequiredException
-      {
-        throw new UnsupportedOperationException("Not supported yet.");
-      }
-
-      @Override
-      public boolean supportsRefresh(OAuth2ProtectedResourceDetails resource)
-      {
-        return false;
-      }
-    });
-  }
-
-
-  static class SOME
-  {
-  }
-}
diff --git a/src/test/java/de/juplo/facebook/MockClientHttpRequestFactory.java b/src/test/java/de/juplo/facebook/MockClientHttpRequestFactory.java
deleted file mode 100644 (file)
index 3162182..0000000
+++ /dev/null
@@ -1,144 +0,0 @@
-package de.juplo.facebook;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URI;
-import java.nio.charset.StandardCharsets;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.client.ClientHttpRequest;
-import org.springframework.http.client.ClientHttpRequestFactory;
-import org.springframework.http.client.ClientHttpResponse;
-
-
-
-/**
- *
- * @author kai
- */
-public class MockClientHttpRequestFactory implements ClientHttpRequestFactory
-{
-  private static final Logger log =
-      LoggerFactory.getLogger(MockClientHttpRequestFactory.class);
-
-  private HttpStatus status = HttpStatus.OK;
-  private HttpHeaders headers = new HttpHeaders();
-  private String body = "";
-
-
-  @Override
-  public ClientHttpRequest createRequest(URI uri, HttpMethod method) throws IOException
-  {
-    return new MockClientHttpRequest(uri, method);
-  }
-
-  public void setStatus(HttpStatus status)
-  {
-    this.status = status;
-  }
-
-  public void setHeaders(HttpHeaders headers)
-  {
-    this.headers = headers;
-  }
-
-  public void addHeader(String name, String value)
-  {
-    headers.add(name, value);
-  }
-
-  public void setBody(String body)
-  {
-    log.trace(body);
-    this.body = body;
-  }
-
-
-  class MockClientHttpRequest implements ClientHttpRequest
-  {
-    private final URI uri;
-    private final HttpMethod method;
-
-
-    public MockClientHttpRequest(URI uri, HttpMethod method)
-    {
-      this.uri = uri;
-      this.method = method;
-    }
-
-
-    @Override
-    public ClientHttpResponse execute() throws IOException
-    {
-      return new MockClientHttpResponse();
-    }
-
-    @Override
-    public HttpMethod getMethod()
-    {
-      return method;
-    }
-
-    @Override
-    public URI getURI()
-    {
-      return uri;
-    }
-
-    @Override
-    public HttpHeaders getHeaders()
-    {
-      return headers;
-    }
-
-    @Override
-    public OutputStream getBody() throws IOException
-    {
-      throw new UnsupportedOperationException("Not supported yet.");
-    }
-  }
-
-
-  class MockClientHttpResponse implements ClientHttpResponse
-  {
-    @Override
-    public HttpStatus getStatusCode() throws IOException
-    {
-      return status;
-    }
-
-    @Override
-    public int getRawStatusCode() throws IOException
-    {
-      return status.value();
-    }
-
-    @Override
-    public String getStatusText() throws IOException
-    {
-      return status.getReasonPhrase();
-    }
-
-    @Override
-    public void close()
-    {
-    }
-
-    @Override
-    public InputStream getBody() throws IOException
-    {
-      return new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
-    }
-
-    @Override
-    public HttpHeaders getHeaders()
-    {
-      return headers;
-    }
-  }
-}
diff --git a/src/test/java/de/juplo/facebook/client/GraphApiErrorHandlerTest.java b/src/test/java/de/juplo/facebook/client/GraphApiErrorHandlerTest.java
new file mode 100644 (file)
index 0000000..ddd19a6
--- /dev/null
@@ -0,0 +1,708 @@
+package de.juplo.facebook.client;
+
+import de.juplo.facebook.exceptions.UnsupportedGetRequestException;
+import de.juplo.facebook.exceptions.UnexpectedErrorException;
+import de.juplo.facebook.exceptions.RateExceededException;
+import de.juplo.facebook.exceptions.GraphApiException;
+import de.juplo.facebook.exceptions.UnknownErrorException;
+import de.juplo.facebook.exceptions.PageMigratedException;
+import java.util.Date;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.Resource;
+import static org.junit.Assert.*;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.oauth2.client.OAuth2RestTemplate;
+import org.springframework.security.oauth2.client.http.OAuth2ErrorHandler;
+import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
+import org.springframework.security.oauth2.client.resource.UserApprovalRequiredException;
+import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
+import org.springframework.security.oauth2.client.token.AccessTokenProvider;
+import org.springframework.security.oauth2.client.token.AccessTokenRequest;
+import org.springframework.security.oauth2.common.OAuth2AccessToken;
+import static org.springframework.security.oauth2.common.OAuth2AccessToken.OAUTH2_TYPE;
+import org.springframework.security.oauth2.common.OAuth2RefreshToken;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.web.client.HttpClientErrorException;
+
+
+
+/**
+ *
+ * @author kai
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(
+  locations = {
+    "classpath:/spring/test-facebook-error-handler.xml"
+    })
+public class GraphApiErrorHandlerTest
+{
+  private static final Logger log =
+      LoggerFactory.getLogger(GraphApiErrorHandlerTest.class);
+
+  @Resource
+  private OAuth2RestTemplate clientTemplate;
+
+  private MockClientHttpRequestFactory requestFactory;
+
+
+  @Test
+  public void testError1()
+  {
+    log.info("testError1");
+
+
+    requestFactory.setBody(
+        "{\n" +
+        "  \"error\":\n" +
+        "  {\n" +
+        "    \"message\": \"An unknown error has occurred.\",\n" +
+        "    \"type\": \"OAuthException\",\n" +
+        "    \"code\": 1\n" +
+        "  }\n" +
+        "}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(UnknownErrorException e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertEquals(1, e.getCode());
+      assertEquals("An unknown error has occurred.", e.getMessage());
+      assertEquals("OAuthException", e.getType());
+    }
+  }
+
+  @Test
+  public void testError2()
+  {
+    log.info("testError2");
+
+
+    requestFactory.setBody(
+        "{\n" +
+        "  \"error\":\n" +
+        "  {\n" +
+        "    \"message\": \"An unexpected error has occurred. Please retry your request later.\",\n" +
+        "    \"type\": \"OAuthException\",\n" +
+        "    \"code\": 2\n" +
+        "  }\n" +
+        "}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(UnexpectedErrorException e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertEquals(2, e.getCode());
+      assertEquals("An unexpected error has occurred. Please retry your request later.", e.getMessage());
+      assertEquals("OAuthException", e.getType());
+    }
+  }
+
+  @Test
+  public void testError21()
+  {
+    log.info("testError21");
+
+
+    requestFactory.setBody(
+        "{\n" +
+        "  \"error\":\n" +
+        "  {\n" +
+        "    \"message\": \"(#21) Page ID 590408587650316 was migrated to page ID 1421620791415603.  Please update your API calls to the new ID\",\n" +
+        "    \"type\": \"OAuthException\",\n" +
+        "    \"code\": 21\n" +
+        "  }\n" +
+        "}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(PageMigratedException e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertEquals(21, e.getCode());
+      assertEquals("(#21) Page ID 590408587650316 was migrated to page ID 1421620791415603.  Please update your API calls to the new ID", e.getMessage());
+      assertEquals("OAuthException", e.getType());
+    }
+  }
+
+  @Test
+  public void testError100()
+  {
+    log.info("testError100");
+
+
+    requestFactory.setBody(
+        "{\n" +
+        "  \"error\":\n" +
+        "  {\n" +
+        "    \"message\": \"Unsupported get request.\",\n" +
+        "    \"type\": \"GraphMethodException\",\n" +
+        "    \"code\": 100\n" +
+        "  }\n" +
+        "}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(UnsupportedGetRequestException e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertEquals(100, e.getCode());
+      assertEquals("Unsupported get request.", e.getMessage());
+      assertEquals("GraphMethodException", e.getType());
+    }
+  }
+
+  @Test
+  public void testError613()
+  {
+    log.info("testError613");
+
+
+    requestFactory.setBody(
+        "{\n" +
+        "  \"error\":\n" +
+        "  {\n" +
+        "    \"message\": \"(#613) Calls to stream have exceeded the rate of 600 calls per 600 seconds.\",\n" +
+        "    \"type\": \"OAuthException\",\n" +
+        "    \"code\": 613\n" +
+        "  }\n" +
+        "}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(RateExceededException e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertEquals(613, e.getCode());
+      assertEquals("(#613) Calls to stream have exceeded the rate of 600 calls per 600 seconds.", e.getMessage());
+      assertEquals("OAuthException", e.getType());
+    }
+  }
+
+  @Test
+  public void testUnmappedError()
+  {
+    log.info("testUnmappedError");
+
+
+    requestFactory.setBody(
+        "{\n" +
+        "  \"error\":\n" +
+        "  {\n" +
+        "    \"message\": \"This error does not exist.\",\n" +
+        "    \"type\": \"NonexistentException\",\n" +
+        "    \"code\": 999999999\n" +
+        "  }\n" +
+        "}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(GraphApiException e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertEquals(999999999, e.getCode());
+      assertEquals("This error does not exist.", e.getMessage());
+      assertEquals("NonexistentException", e.getType());
+    }
+  }
+
+  @Test
+  public void testInvlalidErrors()
+  {
+    log.info("testInvalidErrors");
+
+
+    requestFactory.setBody(
+        "{\n" +
+        "  \"error\":\n" +
+        "  {\n" +
+        "    \"message\": null,\n" +
+        "    \"type\": \"Whatever\",\n" +
+        "    \"code\": 999999999\n" +
+        "  }\n" +
+        "}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+
+
+    requestFactory.setBody(
+        "{\n" +
+        "  \"error\":\n" +
+        "  {\n" +
+        "    \"type\": \"Whatever\",\n" +
+        "    \"code\": 999999999\n" +
+        "  }\n" +
+        "}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+
+
+    requestFactory.setBody(
+        "{\n" +
+        "  \"error\":\n" +
+        "  {\n" +
+        "    \"message\": \"Not a Graph-Api-Exception.\",\n" +
+        "    \"type\": null,\n" +
+        "    \"code\": 999999999\n" +
+        "  }\n" +
+        "}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+
+
+    requestFactory.setBody(
+        "{\n" +
+        "  \"error\":\n" +
+        "  {\n" +
+        "    \"message\": \"Not a Graph-Api-Exception.\",\n" +
+        "    \"code\": 999999999\n" +
+        "  }\n" +
+        "}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+
+
+    requestFactory.setBody(
+        "{\n" +
+        "  \"error\":\n" +
+        "  {\n" +
+        "    \"message\": \"Not a Graph-Api-Exception.\",\n" +
+        "    \"type\": \"Whatever\",\n" +
+        "    \"code\": \"some string\"\n" +
+        "  }\n" +
+        "}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+
+
+    requestFactory.setBody(
+        "{\n" +
+        "  \"error\":\n" +
+        "  {\n" +
+        "    \"message\": \"Not a Graph-Api-Exception.\",\n" +
+        "    \"type\": \"Whatever\",\n" +
+        "    \"code\": 9.9\n" +
+        "  }\n" +
+        "}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+
+
+    requestFactory.setBody(
+        "{\n" +
+        "  \"error\":\n" +
+        "  {\n" +
+        "    \"message\": \"Not a Graph-Api-Exception.\",\n" +
+        "    \"type\": \"Whatever\",\n" +
+        "    \"code\": null\n" +
+        "  }\n" +
+        "}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+
+
+    requestFactory.setBody(
+        "{\n" +
+        "  \"error\":\n" +
+        "  {\n" +
+        "    \"message\": \"Not a Graph-Api-Exception.\",\n" +
+        "    \"type\": \"Whatever\"\n" +
+        "  }\n" +
+        "}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+
+
+    requestFactory.setBody("{\"error\":{\"message\":null}}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+
+
+    requestFactory.setBody("{\"error\":{\"type\":null}}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+
+
+    requestFactory.setBody("{\"error\":{\"code\":null}}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+
+
+    requestFactory.setBody("{\"error\":{}}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+
+
+    requestFactory.setBody("{\"error\":\"some message\"}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+
+
+    requestFactory.setBody("{\"error\":null}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+
+
+    requestFactory.setBody("{\"some filed\":\"some message\"}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+
+
+    requestFactory.setBody("{}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+
+
+    requestFactory.setBody("");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpClientErrorException e)
+    {
+      log.debug("{}", e.toString());
+    }
+    catch(Exception e)
+    {
+      fail("A wrong exception was thrown: " + e.toString());
+    }
+  }
+
+
+  @Before
+  public void setUp()
+  {
+    requestFactory = new MockClientHttpRequestFactory();
+    requestFactory.setStatus(HttpStatus.BAD_REQUEST);
+    requestFactory.addHeader("Content-Type", "application/json");
+    clientTemplate.setRequestFactory(requestFactory);
+
+    clientTemplate.setErrorHandler(
+        new GraphApiErrorHandler(
+            (OAuth2ErrorHandler)clientTemplate.getErrorHandler()
+            )
+        );
+
+    clientTemplate.setAccessTokenProvider(new AccessTokenProvider()
+    {
+      @Override
+      public OAuth2AccessToken obtainAccessToken(
+          OAuth2ProtectedResourceDetails details,
+          AccessTokenRequest parameters
+          )
+          throws
+            UserRedirectRequiredException,
+            UserApprovalRequiredException,
+            AccessDeniedException
+      {
+        return new OAuth2AccessToken() {
+
+          @Override
+          public Map<String, Object> getAdditionalInformation()
+          {
+            throw new UnsupportedOperationException("Not supported yet.");
+          }
+
+          @Override
+          public Set<String> getScope()
+          {
+            throw new UnsupportedOperationException("Not supported yet.");
+          }
+
+          @Override
+          public OAuth2RefreshToken getRefreshToken()
+          {
+            throw new UnsupportedOperationException("Not supported yet.");
+          }
+
+          @Override
+          public String getTokenType()
+          {
+            return OAUTH2_TYPE;
+          }
+
+          @Override
+          public boolean isExpired()
+          {
+            return false;
+          }
+
+          @Override
+          public Date getExpiration()
+          {
+            throw new UnsupportedOperationException("Not supported yet.");
+          }
+
+          @Override
+          public int getExpiresIn()
+          {
+            throw new UnsupportedOperationException("Not supported yet.");
+          }
+
+          @Override
+          public String getValue()
+          {
+            return "ANY";
+          }
+        };
+      }
+
+      @Override
+      public boolean supportsResource(OAuth2ProtectedResourceDetails resource)
+      {
+        return true;
+      }
+
+      @Override
+      public OAuth2AccessToken refreshAccessToken(
+          OAuth2ProtectedResourceDetails resource,
+          OAuth2RefreshToken refreshToken,
+          AccessTokenRequest request
+          )
+          throws
+            UserRedirectRequiredException
+      {
+        throw new UnsupportedOperationException("Not supported yet.");
+      }
+
+      @Override
+      public boolean supportsRefresh(OAuth2ProtectedResourceDetails resource)
+      {
+        return false;
+      }
+    });
+  }
+
+
+  static class SOME
+  {
+  }
+}
diff --git a/src/test/java/de/juplo/facebook/client/MockClientHttpRequestFactory.java b/src/test/java/de/juplo/facebook/client/MockClientHttpRequestFactory.java
new file mode 100644 (file)
index 0000000..7fee109
--- /dev/null
@@ -0,0 +1,144 @@
+package de.juplo.facebook.client;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.ClientHttpResponse;
+
+
+
+/**
+ *
+ * @author kai
+ */
+public class MockClientHttpRequestFactory implements ClientHttpRequestFactory
+{
+  private static final Logger log =
+      LoggerFactory.getLogger(MockClientHttpRequestFactory.class);
+
+  private HttpStatus status = HttpStatus.OK;
+  private HttpHeaders headers = new HttpHeaders();
+  private String body = "";
+
+
+  @Override
+  public ClientHttpRequest createRequest(URI uri, HttpMethod method) throws IOException
+  {
+    return new MockClientHttpRequest(uri, method);
+  }
+
+  public void setStatus(HttpStatus status)
+  {
+    this.status = status;
+  }
+
+  public void setHeaders(HttpHeaders headers)
+  {
+    this.headers = headers;
+  }
+
+  public void addHeader(String name, String value)
+  {
+    headers.add(name, value);
+  }
+
+  public void setBody(String body)
+  {
+    log.trace(body);
+    this.body = body;
+  }
+
+
+  class MockClientHttpRequest implements ClientHttpRequest
+  {
+    private final URI uri;
+    private final HttpMethod method;
+
+
+    public MockClientHttpRequest(URI uri, HttpMethod method)
+    {
+      this.uri = uri;
+      this.method = method;
+    }
+
+
+    @Override
+    public ClientHttpResponse execute() throws IOException
+    {
+      return new MockClientHttpResponse();
+    }
+
+    @Override
+    public HttpMethod getMethod()
+    {
+      return method;
+    }
+
+    @Override
+    public URI getURI()
+    {
+      return uri;
+    }
+
+    @Override
+    public HttpHeaders getHeaders()
+    {
+      return headers;
+    }
+
+    @Override
+    public OutputStream getBody() throws IOException
+    {
+      throw new UnsupportedOperationException("Not supported yet.");
+    }
+  }
+
+
+  class MockClientHttpResponse implements ClientHttpResponse
+  {
+    @Override
+    public HttpStatus getStatusCode() throws IOException
+    {
+      return status;
+    }
+
+    @Override
+    public int getRawStatusCode() throws IOException
+    {
+      return status.value();
+    }
+
+    @Override
+    public String getStatusText() throws IOException
+    {
+      return status.getReasonPhrase();
+    }
+
+    @Override
+    public void close()
+    {
+    }
+
+    @Override
+    public InputStream getBody() throws IOException
+    {
+      return new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Override
+    public HttpHeaders getHeaders()
+    {
+      return headers;
+    }
+  }
+}