/*
* Copyright (c) 2012 Socialize Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.socialize.provider;
import android.content.Context;
import com.socialize.android.ioc.IBeanFactory;
import com.socialize.api.*;
import com.socialize.api.action.ActionType;
import com.socialize.auth.*;
import com.socialize.config.SocializeConfig;
import com.socialize.entity.*;
import com.socialize.error.SocializeApiError;
import com.socialize.error.SocializeException;
import com.socialize.log.SocializeLogger;
import com.socialize.net.HttpClientFactory;
import com.socialize.util.HttpUtils;
import com.socialize.util.IOUtils;
import com.socialize.util.JSONParser;
import com.socialize.util.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.Map.Entry;
/**
* @author Jason Polites
*
* @param <T>
*/
public abstract class BaseSocializeProvider<T extends SocializeObject> implements SocializeProvider<T> {
public static final String JSON_ATTR_ERRORS = "errors";
public static final String JSON_ATTR_ITEMS = "items";
public static final String JSON_ATTR_COUNT = "total_count";
private SocializeObjectFactory<User> userFactory;
private IBeanFactory<AuthProviderData> authProviderDataFactory;
private AuthProviderInfoBuilder authProviderInfoBuilder;
private ErrorFactory errorFactory;
private HttpClientFactory clientFactory;
private SocializeSessionFactory sessionFactory;
private SocializeRequestFactory<T> requestFactory;
private JSONParser jsonParser;
private SocializeLogger logger;
private HttpUtils httpUtils;
private IOUtils ioUtils;
private SocializeSessionPersister sessionPersister;
private SocializeConfig config;
private WeakReference<Context> context;
public BaseSocializeProvider() {
super();
}
@Override
public void init(Context context) {
this.context = new WeakReference<Context>(context);
}
public void setUserFactory(SocializeObjectFactory<User> userFactory) {
this.userFactory = userFactory;
}
public void setClientFactory(HttpClientFactory clientFactory) {
this.clientFactory = clientFactory;
}
public void setRequestFactory(SocializeRequestFactory<T> requestFactory) {
this.requestFactory = requestFactory;
}
public void setJsonParser(JSONParser jsonParser) {
this.jsonParser = jsonParser;
}
public void setHttpUtils(HttpUtils httpUtils) {
this.httpUtils = httpUtils;
}
public void setIoUtils(IOUtils ioUtils) {
this.ioUtils = ioUtils;
}
public void setConfig(SocializeConfig config) {
this.config = config;
}
public void setErrorFactory(ErrorFactory errorFactory) {
this.errorFactory = errorFactory;
}
public void setSessionFactory(SocializeSessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Override
public SocializeSession authenticate(String endpoint, String key, String secret, String uuid, String advertiserId) throws SocializeException {
if(authProviderDataFactory == null) {
throw new SocializeException("Socialize not initialized");
}
AuthProviderData data = authProviderDataFactory.getBean();
if(data == null) {
throw new SocializeException("Socialize not initialized");
}
data.setAuthProviderInfo(authProviderInfoBuilder.getFactory(AuthProviderType.SOCIALIZE).getInstanceForRead());
return authenticate(endpoint, key, secret, data, uuid, advertiserId);
}
@Override
public WritableSession loadSession(String endpoint, String key, String secret) throws SocializeException {
if(sessionPersister != null) {
WritableSession loaded = sessionPersister.load(context.get());
// Verify that the key/secret matches
if(loaded != null) {
String loadedKey = loaded.getConsumerKey();
String loadedSecret = loaded.getConsumerSecret();
String loadedHost = loaded.getHost();
String host = config.getProperty(SocializeConfig.API_HOST);
if(loadedKey != null &&
loadedKey.equals(key) &&
loadedSecret != null &&
loadedSecret.equals(secret) &&
loadedHost != null &&
loadedHost.equals(host)) {
return loaded;
}
}
}
return null;
}
@Override
public boolean validateSession(SocializeSession session, AuthProviderData data) {
AuthProviderInfo info = data.getAuthProviderInfo();
if(info != null) {
if(info.getType().equals(AuthProviderType.SOCIALIZE)) {
return true;
}
return validateSessionAuthData(session, data, info);
}
else {
return false;
}
}
public boolean validateSessionAuthData(SocializeSession loaded, AuthProviderData data, AuthProviderInfo info) {
UserProviderCredentialsMap userProviderCredentialsMap = loaded.getUserProviderCredentials();
if(userProviderCredentialsMap != null) {
UserProviderCredentials userProviderCredentials = userProviderCredentialsMap.get(info.getType());
if(userProviderCredentials != null && userProviderCredentials.getAuthProviderInfo().matches(info)) {
boolean ok = true;
String token3rdParty = data.getToken3rdParty();
String secret3rdParty = data.getSecret3rdParty();
if(!StringUtils.isEmpty(token3rdParty)) {
ok = userProviderCredentials.getAccessToken().equals(token3rdParty);
}
if(ok && !StringUtils.isEmpty(secret3rdParty)) {
ok = userProviderCredentials.getTokenSecret().equals(secret3rdParty);
}
return ok;
}
}
return false;
}
// Mockable
protected AuthProviderData newAuthProviderData() {
return new AuthProviderData();
}
@Override
public void clearSession(AuthProviderType type) {
if(sessionPersister != null) {
sessionPersister.delete(context.get(), type);
}
}
@Override
public void clearSession() {
if(sessionPersister != null) {
sessionPersister.delete(context.get());
}
}
public void saveSession(SocializeSession session) {
if(sessionPersister != null) {
sessionPersister.save(context.get(), session);
}
}
@Override
public SocializeSession authenticate(String endpoint, String key, String secret, AuthProviderData data, String uuid, String advertiserId) throws SocializeException {
try {
SessionLock.lock();
WritableSession session = loadSession(endpoint, key, secret);
if(session != null) {
if(validateSession(session, data)) {
return session;
}
else {
session = setProviderCredentialsForUser(data, session);
}
}
if(session == null) {
session = sessionFactory.create(key, secret, data);
}
endpoint = prepareEndpoint(session, endpoint, true);
if(!clientFactory.isDestroyed()) {
HttpClient client = clientFactory.getClient();
HttpEntity entity = null;
try {
HttpUriRequest request = requestFactory.getAuthRequest(session, endpoint, uuid, advertiserId, data);
if(logger != null && logger.isDebugEnabled()) {
logger.debug("Calling authenticate endpoint for device [" +
uuid +
"]");
}
HttpResponse response = executeRequest(client, request);
if(logger != null && logger.isDebugEnabled()) {
logger.debug("RESPONSE CODE: " + response.getStatusLine().getStatusCode());
}
entity = response.getEntity();
if(httpUtils.isHttpError(response)) {
if(sessionPersister != null && httpUtils.isAuthError(response)) {
sessionPersister.delete(context.get());
}
String msg = ioUtils.readSafe(entity.getContent());
throw new SocializeApiError(httpUtils, response.getStatusLine().getStatusCode(), msg);
}
else {
String responseData = ioUtils.readSafe(entity.getContent());
if(logger != null && logger.isDebugEnabled()) {
logger.debug("RESPONSE: " + responseData);
}
JSONObject json = jsonParser.parseObject(responseData);
User user = userFactory.fromJSON(json.getJSONObject("user"));
String oauth_token = json.getString("oauth_token");
String oauth_token_secret = json.getString("oauth_token_secret");
if(StringUtils.isEmpty(oauth_token)) {
throw new SocializeException("oauth_token was empty in response from server");
}
if(StringUtils.isEmpty(oauth_token_secret)) {
throw new SocializeException("oauth_token_secret was empty in response from server");
}
session.setConsumerToken(oauth_token);
session.setConsumerTokenSecret(oauth_token_secret);
session.setUser(user);
setProviderCredentialsForUser(data, session);
// Ensure the user credentials match the user auth data returned from the server
verifyProviderCredentialsForUser(session, user);
saveSession(session);
}
}
catch (Exception e) {
throw SocializeException.wrap(e);
}
finally {
closeEntity(entity);
}
}
else {
if(logger != null) {
logger.warn("Attempt to access HttpClientFactory that was already destroyed");
}
}
return session;
}
finally {
SessionLock.unlock();
}
}
protected WritableSession setProviderCredentialsForUser(AuthProviderData data, WritableSession session) {
AuthProviderInfo info = data.getAuthProviderInfo();
if(info != null) {
DefaultUserProviderCredentials userProviderCredentials = new DefaultUserProviderCredentials();
userProviderCredentials.setAccessToken(data.getToken3rdParty());
userProviderCredentials.setTokenSecret(data.getSecret3rdParty());
userProviderCredentials.setUserId(data.getUserId3rdParty());
UserProviderCredentials current = session.getUserProviderCredentials().get(info.getType());
if(current != null) {
AuthProviderInfo authProviderInfo = current.getAuthProviderInfo();
if(authProviderInfo != null) {
authProviderInfo.merge(data.getAuthProviderInfo());
}
userProviderCredentials.setAuthProviderInfo(authProviderInfo);
}
else {
userProviderCredentials.setAuthProviderInfo(data.getAuthProviderInfo());
}
session.getUserProviderCredentials().put(info.getType(), userProviderCredentials);
}
else {
// Legacy
session = null;
}
return session;
}
protected void verifyProviderCredentialsForUser(WritableSession session, User user) {
List<UserAuthData> authData = user.getAuthData();
UserProviderCredentialsMap credentials = session.getUserProviderCredentials();
if(credentials != null) {
if(authData != null) {
Map<AuthProviderType, UserProviderCredentials> validCreds = new LinkedHashMap<AuthProviderType, UserProviderCredentials>();
for (UserAuthData userAuthData : authData) {
UserProviderCredentials creds = credentials.get(userAuthData.getAuthProviderType());
if(creds != null) {
validCreds.put(userAuthData.getAuthProviderType(), creds);
}
}
// Clear and reset
credentials.removeAll();
Set<Entry<AuthProviderType, UserProviderCredentials>> entrySet = validCreds.entrySet();
for (Entry<AuthProviderType, UserProviderCredentials> entry : entrySet) {
credentials.put(entry.getKey(), entry.getValue());
}
}
else {
credentials.removeAll();
}
// Set back to session
session.setUserProviderCredentials(credentials);
}
}
@Override
public T get(SocializeSession session, String endpoint, String id, ActionType type) throws SocializeException {
endpoint = prepareEndpoint(session, endpoint);
HttpUriRequest get = requestFactory.getGetRequest(session, endpoint, id);
return doGetTypeRequest(get, type);
}
@Override
public T get(SocializeSession session, String endpoint, String id) throws SocializeException {
return get(session, endpoint, id, ActionType.UNKNOWN);
}
@Override
public void delete(SocializeSession session, String endpoint, String id) throws SocializeException {
HttpEntity entity = null;
if(!clientFactory.isDestroyed()) {
try {
endpoint = prepareEndpoint(session, endpoint);
HttpClient client = clientFactory.getClient();
HttpUriRequest del = requestFactory.getDeleteRequest(session, endpoint, id);
HttpResponse response = client.execute(del);
entity = response.getEntity();
if(httpUtils.isHttpError(response)) {
if(sessionPersister != null && httpUtils.isAuthError(response)) {
sessionPersister.delete(context.get());
}
String msg = ioUtils.readSafe(entity.getContent());
throw new SocializeApiError(httpUtils, response.getStatusLine().getStatusCode(), msg);
}
}
catch (Exception e) {
throw SocializeException.wrap(e);
}
finally {
closeEntity(entity);
}
}
else {
if(logger != null) {
logger.warn("Attempt to access HttpClientFactory that was already destroyed");
}
}
}
@Override
public ListResult<T> list(SocializeSession session, String endpoint, int startIndex, int endIndex) throws SocializeException {
endpoint = prepareEndpoint(session, endpoint);
HttpUriRequest request = requestFactory.getListRequest(session, endpoint, startIndex, endIndex);
return doListTypeRequest(request, ActionType.UNKNOWN);
}
@Override
public ListResult<T> list(SocializeSession session, String endpoint, String key, String[] ids, String idKey, Map<String, String> extraParams, int startIndex, int endIndex) throws SocializeException {
endpoint = prepareEndpoint(session, endpoint);
HttpUriRequest request = requestFactory.getListRequest(session, endpoint, key, ids, idKey, extraParams, startIndex, endIndex);
return doListTypeRequest(request, ActionType.UNKNOWN);
}
@Override
public ListResult<T> list(SocializeSession session, String endpoint, String key, String[] ids, int startIndex, int endIndex) throws SocializeException {
endpoint = prepareEndpoint(session, endpoint);
HttpUriRequest request = requestFactory.getListRequest(session, endpoint, key, ids, startIndex, endIndex);
return doListTypeRequest(request, ActionType.UNKNOWN);
}
@Override
public ListResult<T> put(SocializeSession session, String endpoint, T object) throws SocializeException {
endpoint = prepareEndpoint(session, endpoint);
HttpUriRequest request = requestFactory.getPutRequest(session, endpoint, object);
return doListTypeRequest(request, ActionType.UNKNOWN);
}
@Override
public ListResult<T> put(SocializeSession session, String endpoint, Collection<T> objects) throws SocializeException {
endpoint = prepareEndpoint(session, endpoint);
HttpUriRequest request = requestFactory.getPutRequest(session, endpoint, objects);
return doListTypeRequest(request, ActionType.UNKNOWN);
}
@Override
public ListResult<T> post(SocializeSession session, String endpoint, T object, boolean jsonResponse) throws SocializeException {
endpoint = prepareEndpoint(session, endpoint);
HttpUriRequest request = requestFactory.getPostRequest(session, endpoint, object);
return doListTypeRequest(request, ActionType.UNKNOWN, jsonResponse);
}
@Override
public T putAsPost(SocializeSession session, String endpoint, T object) throws SocializeException {
endpoint = prepareEndpoint(session, endpoint);
HttpUriRequest request = requestFactory.getPostRequest(session, endpoint, object);
return doGetTypeRequest(request, ActionType.UNKNOWN);
}
@Override
public ListResult<T> post(SocializeSession session, String endpoint, Collection<T> objects, boolean isJSONResponse) throws SocializeException {
endpoint = prepareEndpoint(session, endpoint);
HttpUriRequest request = requestFactory.getPostRequest(session, endpoint, objects);
return doListTypeRequest(request, ActionType.UNKNOWN);
}
private T doGetTypeRequest(HttpUriRequest request, ActionType actionType) throws SocializeException {
HttpEntity entity = null;
if(!clientFactory.isDestroyed()) {
try {
HttpClient client = clientFactory.getClient();
HttpResponse response = executeRequest(client, request);
if(logger != null && logger.isDebugEnabled()) {
logger.debug("RESPONSE CODE: " + response.getStatusLine().getStatusCode());
}
entity = response.getEntity();
if(httpUtils.isHttpError(response)) {
if(sessionPersister != null && httpUtils.isAuthError(response)) {
sessionPersister.delete(context.get());
}
String msg = ioUtils.readSafe(entity.getContent());
throw new SocializeApiError(httpUtils, response.getStatusLine().getStatusCode(), msg);
}
else {
String responseData = ioUtils.readSafe(entity.getContent());
if(logger != null && logger.isDebugEnabled()) {
logger.debug("RESPONSE: " + responseData);
}
JSONObject json = jsonParser.parseObject(responseData);
return fromJSON(json, actionType);
}
}
catch (Exception e) {
throw SocializeException.wrap(e);
}
finally {
closeEntity(entity);
}
}
else {
if(logger != null) {
logger.warn("Attempt to access HttpClientFactory that was already destroyed");
}
return null;
}
}
private HttpResponse executeRequest(HttpClient client, HttpUriRequest request) throws IOException {
if(logger != null && logger.isDebugEnabled()) {
StringBuilder builder = new StringBuilder();
Header[] allHeaders = request.getAllHeaders();
for (Header header : allHeaders) {
builder.append(header.getName());
builder.append(":");
builder.append(header.getValue());
builder.append("\n");
}
if(logger.isDebugEnabled()) {
logger.debug("REQUEST \nurl:[" +
request.getURI().toString() +
"] \nheaders:\n" +
builder.toString());
}
if(request instanceof HttpPost) {
HttpPost post = (HttpPost) request;
HttpEntity entity = post.getEntity();
String requestData = ioUtils.readSafe(entity.getContent());
if(logger.isDebugEnabled()) {
logger.debug("REQUEST \ndata:[" +
requestData +
"]");
}
}
}
return client.execute(request);
}
private ListResult<T> doListTypeRequest(HttpUriRequest request, ActionType type) throws SocializeException {
return doListTypeRequest(request, type, true);
}
private ListResult<T> doListTypeRequest(HttpUriRequest request, ActionType type, boolean isJSONResponse) throws SocializeException {
List<T> results = null;
List<ActionError> errors = null;
HttpEntity entity = null;
ListResult<T> result = null;
if(!clientFactory.isDestroyed()) {
try {
HttpClient client = clientFactory.getClient();
if(logger != null && logger.isDebugEnabled()) {
logger.debug("Request: " + request.getMethod() + " " + request.getRequestLine().getUri());
}
HttpResponse response = executeRequest(client, request);
if(logger != null && logger.isDebugEnabled()) {
logger.debug("RESPONSE CODE: " + response.getStatusLine().getStatusCode());
}
entity = response.getEntity();
if(httpUtils.isHttpError(response)) {
if(sessionPersister != null && httpUtils.isAuthError(response)) {
sessionPersister.delete(context.get());
}
String msg = ioUtils.readSafe(entity.getContent());
throw new SocializeApiError(httpUtils, response.getStatusLine().getStatusCode(), msg);
}
else {
result = new ListResult<T>();
if(isJSONResponse) {
// Read the json just for logging
String json = ioUtils.readSafe(entity.getContent());
if(logger != null && logger.isDebugEnabled()) {
logger.debug("RESPONSE: " + json);
}
if(!StringUtils.isEmpty(json)) {
JSONObject object;
try {
object = jsonParser.parseObject(json);
}
catch (JSONException je) {
throw new SocializeException("Failed to parse response as JSON [" +
json +
"]", je);
}
if(object.has(JSON_ATTR_ERRORS) && !object.isNull(JSON_ATTR_ERRORS)) {
JSONArray errorList = object.getJSONArray(JSON_ATTR_ERRORS);
int length = errorList.length();
errors = new ArrayList<ActionError>(length);
for (int i = 0; i < length; i++) {
JSONObject jsonObject = errorList.getJSONObject(i);
ActionError error = errorFactory.fromJSON(jsonObject);
errors.add(error);
}
result.setErrors(errors);
}
if(object.has(JSON_ATTR_ITEMS) && !object.isNull(JSON_ATTR_ITEMS)) {
JSONArray list = object.getJSONArray(JSON_ATTR_ITEMS);
int length = list.length();
results = new ArrayList<T>(length);
for (int i = 0; i < length; i++) {
results.add(fromJSON(list.getJSONObject(i), type));
}
result.setItems(results);
}
if(object.has(JSON_ATTR_COUNT) && !object.isNull(JSON_ATTR_COUNT)) {
result.setTotalCount(object.getInt(JSON_ATTR_COUNT));
}
}
}
}
}
catch (Throwable e) {
throw SocializeException.wrap(e);
}
finally {
closeEntity(entity);
}
return result;
}
else {
if(logger != null) {
logger.warn("Attempt to access HttpClientFactory that was already destroyed");
}
return null;
}
}
public void setLogger(SocializeLogger logger) {
this.logger = logger;
}
public void setSessionPersister(SocializeSessionPersister sessionPersister) {
this.sessionPersister = sessionPersister;
}
public void setAuthProviderDataFactory(IBeanFactory<AuthProviderData> authProviderDataFactory) {
this.authProviderDataFactory = authProviderDataFactory;
}
public void setAuthProviderInfoBuilder(AuthProviderInfoBuilder authProviderInfoBuilder) {
this.authProviderInfoBuilder = authProviderInfoBuilder;
}
private final String prepareEndpoint(SocializeSession session, String endpoint) {
return prepareEndpoint(session, endpoint, false);
}
private final String prepareEndpoint(SocializeSession session, String endpoint, boolean secure) {
return prepareEndpoint(session.getHost(), endpoint, secure);
}
private final String prepareEndpoint(String host, String endpoint, boolean secure) {
endpoint = endpoint.trim();
if(StringUtils.isEmpty(host)) {
logger.warn("The session did not have a host configured, using the config");
host = config.getProperty(SocializeConfig.API_HOST);
}
if(host != null) {
if(!host.startsWith("http")) {
if(secure) {
host = "https://" + host;
}
else {
host = "http://" + host;
}
}
if(!host.endsWith("/")) {
if(!endpoint.startsWith("/")) {
host += "/";
}
}
else if (endpoint.startsWith("/")) {
endpoint = endpoint.substring(1, endpoint.length());
}
endpoint = host + endpoint;
}
else {
logger.error("Could not locate host property in session or config!");
}
if(!endpoint.endsWith("/")) {
endpoint += "/";
}
return endpoint;
}
private final void closeEntity(HttpEntity entity) {
if(entity != null) {
try {
entity.consumeContent();
}
catch (IOException e) {
if(logger != null) {
logger.warn("Failed to fully consume http response content", e);
}
}
}
}
public SocializeSessionPersister getSessionPersister() {
return sessionPersister;
}
public abstract T fromJSON(JSONObject json, ActionType type) throws JSONException;
}