From: Kai Moritz Date: Thu, 8 Oct 2015 07:12:49 +0000 (+0200) Subject: Refactored classes in thematically packages X-Git-Tag: facebook-utils-2.5.0~14 X-Git-Url: https://juplo.de/gitweb/?p=facebook-utils;a=commitdiff_plain;h=bc0989ddb7bb05e2d95ae4aad4438b4d4806f9dc Refactored classes in thematically packages --- diff --git a/src/main/java/de/juplo/facebook/FacebookUtils.java b/src/main/java/de/juplo/facebook/FacebookUtils.java index a0e6738..b2b8261 100644 --- a/src/main/java/de/juplo/facebook/FacebookUtils.java +++ b/src/main/java/de/juplo/facebook/FacebookUtils.java @@ -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 index 2864843..0000000 --- a/src/main/java/de/juplo/facebook/GraphApiErrorHandler.java +++ /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> messageConverters = - new RestTemplate().getMessageConverters(); - - - public GraphApiErrorHandler(OAuth2ErrorHandler errorHandler) - { - super(null); - this.errorHandler = errorHandler; - } - - - /** - * @param messageConverters the messageConverters to set - */ - @Override - public void setMessageConverters( - List> 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 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 index 18eaf81..0000000 --- a/src/main/java/de/juplo/facebook/GraphApiException.java +++ /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 index 8a1d3bf..0000000 --- a/src/main/java/de/juplo/facebook/GraphApiExceptionJackson1Deserializer.java +++ /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 -{ - - @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 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 index becb51f..0000000 --- a/src/main/java/de/juplo/facebook/GraphApiExceptionJackson2Deserializer.java +++ /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 -{ - 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 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 index 934243c..0000000 --- a/src/main/java/de/juplo/facebook/GraphMethodException.java +++ /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 index 1e80fe3..0000000 --- a/src/main/java/de/juplo/facebook/OAuthException.java +++ /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 index c3da159..0000000 --- a/src/main/java/de/juplo/facebook/PageMigratedException.java +++ /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 index b633069..0000000 --- a/src/main/java/de/juplo/facebook/RateExceededException.java +++ /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 index fcae74a..0000000 --- a/src/main/java/de/juplo/facebook/SignedRequestAwareAuthorizationCodeAccessTokenProvider.java +++ /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 additionalInformation = new HashMap<>(); - try - { - additionalInformation.put( - "issued_at", - new Date(json.get("issued_at").getLongValue()*1000L) - ); - Map 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 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 index 7bad046..0000000 --- a/src/main/java/de/juplo/facebook/SignedRequestAwareUserRedirectRequiredException.java +++ /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 signedRequestData; - - - public SignedRequestAwareUserRedirectRequiredException( - String redirectUri, - Map requestParams, - Map signedRequestData - ) - { - super(redirectUri, requestParams); - this.signedRequestData = signedRequestData; - } - - - public Map 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 index 031195f..0000000 --- a/src/main/java/de/juplo/facebook/UnexpectedErrorException.java +++ /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 index 9790973..0000000 --- a/src/main/java/de/juplo/facebook/UnknownErrorException.java +++ /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 index f1020c1..0000000 --- a/src/main/java/de/juplo/facebook/UnsupportedGetRequestException.java +++ /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 index 0000000..faff55e --- /dev/null +++ b/src/main/java/de/juplo/facebook/client/GraphApiErrorHandler.java @@ -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> messageConverters = + new RestTemplate().getMessageConverters(); + + + public GraphApiErrorHandler(OAuth2ErrorHandler errorHandler) + { + super(null); + this.errorHandler = errorHandler; + } + + + /** + * @param messageConverters the messageConverters to set + */ + @Override + public void setMessageConverters( + List> 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 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 index 0000000..03aa627 --- /dev/null +++ b/src/main/java/de/juplo/facebook/exceptions/GraphApiException.java @@ -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 index 0000000..165e7ce --- /dev/null +++ b/src/main/java/de/juplo/facebook/exceptions/GraphApiExceptionJackson1Deserializer.java @@ -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 +{ + + @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 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 index 0000000..ea0e987 --- /dev/null +++ b/src/main/java/de/juplo/facebook/exceptions/GraphApiExceptionJackson2Deserializer.java @@ -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 +{ + 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 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 index 0000000..21c56c5 --- /dev/null +++ b/src/main/java/de/juplo/facebook/exceptions/GraphMethodException.java @@ -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 index 0000000..4537720 --- /dev/null +++ b/src/main/java/de/juplo/facebook/exceptions/OAuthException.java @@ -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 index 0000000..d327b6d --- /dev/null +++ b/src/main/java/de/juplo/facebook/exceptions/PageMigratedException.java @@ -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 index 0000000..cdda586 --- /dev/null +++ b/src/main/java/de/juplo/facebook/exceptions/RateExceededException.java @@ -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 index 0000000..51fbb12 --- /dev/null +++ b/src/main/java/de/juplo/facebook/exceptions/SignedRequestAwareUserRedirectRequiredException.java @@ -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 signedRequestData; + + + public SignedRequestAwareUserRedirectRequiredException( + String redirectUri, + Map requestParams, + Map signedRequestData + ) + { + super(redirectUri, requestParams); + this.signedRequestData = signedRequestData; + } + + + public Map 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 index 0000000..5aefff6 --- /dev/null +++ b/src/main/java/de/juplo/facebook/exceptions/UnexpectedErrorException.java @@ -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 index 0000000..6e336f4 --- /dev/null +++ b/src/main/java/de/juplo/facebook/exceptions/UnknownErrorException.java @@ -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 index 0000000..1d5b44d --- /dev/null +++ b/src/main/java/de/juplo/facebook/exceptions/UnsupportedGetRequestException.java @@ -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 index 0000000..e187542 --- /dev/null +++ b/src/main/java/de/juplo/facebook/token/SignedRequestAwareAuthorizationCodeAccessTokenProvider.java @@ -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 additionalInformation = new HashMap<>(); + try + { + additionalInformation.put( + "issued_at", + new Date(json.get("issued_at").getLongValue()*1000L) + ); + Map 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 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 index ef849cf..0000000 --- a/src/test/java/de/juplo/facebook/GraphApiErrorHandlerTest.java +++ /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 getAdditionalInformation() - { - throw new UnsupportedOperationException("Not supported yet."); - } - - @Override - public Set 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 index 3162182..0000000 --- a/src/test/java/de/juplo/facebook/MockClientHttpRequestFactory.java +++ /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 index 0000000..ddd19a6 --- /dev/null +++ b/src/test/java/de/juplo/facebook/client/GraphApiErrorHandlerTest.java @@ -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 getAdditionalInformation() + { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Set 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 index 0000000..7fee109 --- /dev/null +++ b/src/test/java/de/juplo/facebook/client/MockClientHttpRequestFactory.java @@ -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; + } + } +}