Implemented FacebookErrorHandler to handle facebook-error-codes
authorKai Moritz <kai@juplo.de>
Mon, 28 Jul 2014 16:38:50 +0000 (18:38 +0200)
committerKai Moritz <kai@juplo.de>
Tue, 6 Oct 2015 06:22:25 +0000 (08:22 +0200)
18 files changed:
pom.xml
src/main/java/de/juplo/facebook/FacebookUtils.java
src/main/java/de/juplo/facebook/GraphApiErrorHandler.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/GraphApiException.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/GraphApiExceptionJackson1Deserializer.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/GraphApiExceptionJackson1Serializer.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Deserializer.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Serializer.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/GraphMethodException.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/OAuthException.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/PageMigratedException.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/UnexpectedErrorException.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/UnknownErrorException.java [new file with mode: 0644]
src/main/java/de/juplo/facebook/UnsupportedGetRequestException.java [new file with mode: 0644]
src/test/java/de/juplo/facebook/GraphApiErrorHandlerTest.java [new file with mode: 0644]
src/test/java/de/juplo/facebook/MockClientHttpRequestFactory.java [new file with mode: 0644]
src/test/resources/logback-test.xml [new file with mode: 0644]
src/test/resources/spring/test-facebook-error-handler.xml [new file with mode: 0644]

diff --git a/pom.xml b/pom.xml
index 9a5597d..65f2569 100644 (file)
--- a/pom.xml
+++ b/pom.xml
     <!-- used versions -->
     <commons-codec.version>1.7</commons-codec.version>
     <jackson.version>2.3.2</jackson.version>
+    <junit.version>4.11</junit.version>
+    <logback.version>1.1.2</logback.version>
     <servlet-api.version>3.0.1</servlet-api.version>
     <slf4j.version>1.7.6</slf4j.version>
     <spring.version>3.2.4.RELEASE</spring.version>
+    <springframework.version>3.2.4.RELEASE</springframework.version>
     <spring-security.version>3.1.3.RELEASE</spring-security.version>
     <spring-security-oauth2.version>1.0.5.RELEASE</spring-security-oauth2.version>
 
       <version>${slf4j.version}</version>
     </dependency>
 
+    <!-- Testing -->
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>${junit.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-test</artifactId>
+      <version>${springframework.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>jcl-over-slf4j</artifactId>
+      <version>${slf4j.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+      <version>${logback.version}</version>
+      <scope>test</scope>
+    </dependency>
+
   </dependencies>
 
 
index bf0a5df..b55eb3b 100644 (file)
@@ -16,6 +16,7 @@ import org.springframework.context.annotation.Configuration;
 import org.springframework.http.converter.HttpMessageConverter;
 import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
 import org.springframework.security.oauth2.client.OAuth2RestTemplate;
+import org.springframework.security.oauth2.client.http.OAuth2ErrorHandler;
 import org.springframework.security.oauth2.client.token.AccessTokenProvider;
 import org.springframework.security.oauth2.client.token.AccessTokenProviderChain;
 import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider;
@@ -112,6 +113,12 @@ public class FacebookUtils
           provider.setObjectMapper(objectMapper);
           chain.add(provider);
           template.setAccessTokenProvider(new AccessTokenProviderChain(chain));
+          log.info("injecting GraphApiErrorHandler");
+          template.setErrorHandler(
+              new GraphApiErrorHandler(
+                  (OAuth2ErrorHandler)template.getErrorHandler()
+                  )
+              );
         }
 
         return bean;
diff --git a/src/main/java/de/juplo/facebook/GraphApiErrorHandler.java b/src/main/java/de/juplo/facebook/GraphApiErrorHandler.java
new file mode 100644 (file)
index 0000000..ce4c98c
--- /dev/null
@@ -0,0 +1,79 @@
+package de.juplo.facebook;
+
+import java.io.IOException;
+import java.util.List;
+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.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 errorHandler.hasError(response);
+  }
+
+  @Override
+  public void handleError(ClientHttpResponse response) throws IOException
+  {
+    HttpMessageConverterExtractor<GraphApiException> extractor =
+        new HttpMessageConverterExtractor<>(
+            GraphApiException.class,
+            messageConverters
+            );
+
+    try
+    {
+      GraphApiException body = extractor.extractData(response);
+      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(response);
+  }
+}
diff --git a/src/main/java/de/juplo/facebook/GraphApiException.java b/src/main/java/de/juplo/facebook/GraphApiException.java
new file mode 100644 (file)
index 0000000..47ea6c3
--- /dev/null
@@ -0,0 +1,64 @@
+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.JsonSerialize(using = GraphApiExceptionJackson1Serializer.class)
+@org.codehaus.jackson.map.annotate.JsonDeserialize(using = GraphApiExceptionJackson1Deserializer.class)
+@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = GraphApiExceptionJackson2Serializer.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
new file mode 100644 (file)
index 0000000..2d0c002
--- /dev/null
@@ -0,0 +1,93 @@
+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();
+      default:  return new GraphApiException(message, type, code);
+    }
+  }
+}
diff --git a/src/main/java/de/juplo/facebook/GraphApiExceptionJackson1Serializer.java b/src/main/java/de/juplo/facebook/GraphApiExceptionJackson1Serializer.java
new file mode 100644 (file)
index 0000000..db7497f
--- /dev/null
@@ -0,0 +1,32 @@
+package de.juplo.facebook;
+
+import java.io.IOException;
+import java.util.Map.Entry;
+import org.codehaus.jackson.JsonGenerator;
+import org.codehaus.jackson.JsonProcessingException;
+import org.codehaus.jackson.map.JsonSerializer;
+import org.codehaus.jackson.map.SerializerProvider;
+
+/**
+ * @author Dave Syer
+ *
+ */
+public class GraphApiExceptionJackson1Serializer extends JsonSerializer<GraphApiException> {
+
+       @Override
+       public void serialize(GraphApiException value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
+                       JsonProcessingException {
+        jgen.writeStartObject();
+               jgen.writeStringField("error", value.getOAuth2ErrorCode());
+               jgen.writeStringField("error_description", value.getMessage());
+               if (value.getAdditionalInformation()!=null) {
+                       for (Entry<String, String> entry : value.getAdditionalInformation().entrySet()) {
+                               String key = entry.getKey();
+                               String add = entry.getValue();
+                               jgen.writeStringField(key, add);                                
+                       }
+               }
+        jgen.writeEndObject();
+       }
+
+}
diff --git a/src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Deserializer.java b/src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Deserializer.java
new file mode 100644 (file)
index 0000000..ec46de3
--- /dev/null
@@ -0,0 +1,100 @@
+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();
+      default:  return new GraphApiException(message, type, code);
+    }
+  }
+}
diff --git a/src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Serializer.java b/src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Serializer.java
new file mode 100644 (file)
index 0000000..23b02d8
--- /dev/null
@@ -0,0 +1,37 @@
+package de.juplo.facebook;
+
+import java.io.IOException;
+import java.util.Map.Entry;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+/**
+ * @author Brian Clozel
+ *
+ */
+public class GraphApiExceptionJackson2Serializer extends StdSerializer<GraphApiException> {
+
+    public GraphApiExceptionJackson2Serializer() {
+        super(GraphApiException.class);
+    }
+
+       @Override
+       public void serialize(GraphApiException value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
+                       JsonProcessingException {
+        jgen.writeStartObject();
+               jgen.writeStringField("error", value.getOAuth2ErrorCode());
+               jgen.writeStringField("error_description", value.getMessage());
+               if (value.getAdditionalInformation()!=null) {
+                       for (Entry<String, String> entry : value.getAdditionalInformation().entrySet()) {
+                               String key = entry.getKey();
+                               String add = entry.getValue();
+                               jgen.writeStringField(key, add);                                
+                       }
+               }
+        jgen.writeEndObject();
+       }
+
+}
diff --git a/src/main/java/de/juplo/facebook/GraphMethodException.java b/src/main/java/de/juplo/facebook/GraphMethodException.java
new file mode 100644 (file)
index 0000000..934243c
--- /dev/null
@@ -0,0 +1,14 @@
+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
new file mode 100644 (file)
index 0000000..1e80fe3
--- /dev/null
@@ -0,0 +1,14 @@
+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
new file mode 100644 (file)
index 0000000..c3da159
--- /dev/null
@@ -0,0 +1,41 @@
+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/UnexpectedErrorException.java b/src/main/java/de/juplo/facebook/UnexpectedErrorException.java
new file mode 100644 (file)
index 0000000..031195f
--- /dev/null
@@ -0,0 +1,14 @@
+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
new file mode 100644 (file)
index 0000000..9790973
--- /dev/null
@@ -0,0 +1,14 @@
+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
new file mode 100644 (file)
index 0000000..f1020c1
--- /dev/null
@@ -0,0 +1,14 @@
+package de.juplo.facebook;
+
+
+/**
+ *
+ * @author kai
+ */
+public class UnsupportedGetRequestException extends GraphMethodException
+{
+  public UnsupportedGetRequestException()
+  {
+    super("Unsupported get request.", 100);
+  }
+}
diff --git a/src/test/java/de/juplo/facebook/GraphApiErrorHandlerTest.java b/src/test/java/de/juplo/facebook/GraphApiErrorHandlerTest.java
new file mode 100644 (file)
index 0000000..b9dc905
--- /dev/null
@@ -0,0 +1,637 @@
+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.http.converter.HttpMessageNotReadableException;
+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.security.oauth2.common.exceptions.OAuth2Exception;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+
+
+/**
+ *
+ * @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 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(OAuth2Exception e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertFalse(e instanceof GraphApiException);
+    }
+
+
+    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(OAuth2Exception e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertFalse(e instanceof GraphApiException);
+    }
+
+
+    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(OAuth2Exception e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertFalse(e instanceof GraphApiException);
+    }
+
+
+    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(OAuth2Exception e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertFalse(e instanceof GraphApiException);
+    }
+
+
+    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(OAuth2Exception e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertFalse(e instanceof GraphApiException);
+    }
+
+
+    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(OAuth2Exception e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertFalse(e instanceof GraphApiException);
+    }
+
+
+    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(OAuth2Exception e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertFalse(e instanceof GraphApiException);
+    }
+
+
+    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(OAuth2Exception e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertFalse(e instanceof GraphApiException);
+    }
+
+
+    requestFactory.setBody("{\"error\":{\"message\":null}}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(OAuth2Exception e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertFalse(e instanceof GraphApiException);
+    }
+
+
+    requestFactory.setBody("{\"error\":{\"type\":null}}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(OAuth2Exception e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertFalse(e instanceof GraphApiException);
+    }
+
+
+    requestFactory.setBody("{\"error\":{\"code\":null}}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(OAuth2Exception e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertFalse(e instanceof GraphApiException);
+    }
+
+
+    requestFactory.setBody("{\"error\":{}}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(OAuth2Exception e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertFalse(e instanceof GraphApiException);
+    }
+
+
+    requestFactory.setBody("{\"error\":\"some message\"}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(OAuth2Exception e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertFalse(e instanceof GraphApiException);
+    }
+
+
+    requestFactory.setBody("{\"error\":null}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(OAuth2Exception e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertFalse(e instanceof GraphApiException);
+    }
+
+
+    requestFactory.setBody("{\"some filed\":\"some message\"}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(OAuth2Exception e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertFalse(e instanceof GraphApiException);
+    }
+
+
+    requestFactory.setBody("{}");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(OAuth2Exception e)
+    {
+      log.debug("{}", e.toString());
+      assertEquals("invalid_request", e.getOAuth2ErrorCode());
+      assertFalse(e instanceof GraphApiException);
+    }
+
+
+    requestFactory.setBody("");
+
+    try
+    {
+      clientTemplate.getForObject("ANY", SOME.class);
+      fail("The expected exception was not thrown");
+    }
+    catch(HttpMessageNotReadableException e)
+    {
+      // TODO: OAuth2ErrorHandler fails, if body contains no valid JSON!
+      log.debug("{}", 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
new file mode 100644 (file)
index 0000000..3162182
--- /dev/null
@@ -0,0 +1,144 @@
+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/resources/logback-test.xml b/src/test/resources/logback-test.xml
new file mode 100644 (file)
index 0000000..0638ecb
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<configuration>
+
+  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+    <encoder>
+      <pattern>%p - %c{0}.%M\(%L\) | %m%n</pattern>
+    </encoder>
+  </appender>
+
+  <logger name="de.juplo.facebook">
+    <level value="trace"/>
+  </logger>
+
+  <root>
+    <level value="info"/>
+    <appender-ref ref="CONSOLE"/>
+  </root>
+
+</configuration>
diff --git a/src/test/resources/spring/test-facebook-error-handler.xml b/src/test/resources/spring/test-facebook-error-handler.xml
new file mode 100644 (file)
index 0000000..e1b5629
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:oauth="http://www.springframework.org/schema/security/oauth2"
+       xsi:schemaLocation="
+           http://www.springframework.org/schema/beans
+           http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
+           http://www.springframework.org/schema/security/oauth2
+           http://www.springframework.org/schema/security/spring-security-oauth2-1.0.xsd
+          ">
+
+
+  <oauth:rest-template id="template" resource="resource"/>
+  <oauth:resource
+      id="resource"
+      type="client_credentials"
+      client-id="EGAL"
+      client-secret="EGAL"
+      authentication-scheme="query"
+      access-token-uri="https://graph.facebook.com/v2.0/oauth/access_token"
+      token-name="oauth_token"
+      scope="read_stream"
+      />
+
+</beans>