/*
* Copyright (C) 2014 SCVNGR, Inc. d/b/a LevelUp
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.scvngr.levelup.core.net;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.scvngr.levelup.core.annotation.LevelUpApi;
import com.scvngr.levelup.core.annotation.LevelUpApi.Contract;
import com.scvngr.levelup.core.annotation.VisibleForTesting;
import com.scvngr.levelup.core.annotation.VisibleForTesting.Visibility;
import com.scvngr.levelup.core.net.AbstractRequest.BadRequestException;
import com.scvngr.levelup.core.util.LogManager;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
import java.util.HashMap;
import java.util.Locale;
/**
* Class for interacting with the LevelUp web service API.
*/
@ThreadSafe
@LevelUpApi(contract = Contract.DRAFT)
public final class LevelUpConnection {
/**
* Application context.
*/
@NonNull
private final Context mContext;
/**
* For Testing: HashMap of the last request that was made with the key of the URL for the
* request.
*/
@GuardedBy("mRequestIntrinsicLock")
@Nullable
private final HashMap<String, AbstractRequest> mLastRequestMap =
new HashMap<String, AbstractRequest>();
/**
* For Testing: HashMap of pre-made responses to use when the String key URL is requested.
*/
@GuardedBy("mResponseIntrinsicLock")
@NonNull
private final HashMap<String, LevelUpResponse> mNextResponseMap =
new HashMap<String, LevelUpResponse>();
/**
* Intrinsic lock for guarding {@link #mNextResponseMap}.
*/
@NonNull
private final Object[] mResponseIntrinsicLock = new Object[0];
/**
* Intrinsic lock for guarding {@link #mLastRequestMap}.
*/
@NonNull
private final Object[] mRequestIntrinsicLock = new Object[0];
/**
* For Testing: If false, if any network activity happens, an exception will be thrown.
*/
private static volatile boolean sIsNetworkEnabled = true;
/**
* For Testing: The next LevelUpConnection instance for {@link #newInstance(Context)}
* to return.
*/
@Nullable
private static volatile LevelUpConnection sNextInstance;
/**
* Clients should use {@link #newInstance(Context)}.
*
* @param context Application context.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
/* package */LevelUpConnection(@NonNull final Context context) {
mContext = context.getApplicationContext();
}
/**
* Method to get a new instance of LevelUpConnection.
*
* @param context the context to use to get package info
* @return an instance of {@link LevelUpConnection} to use to interact with the
* LevelUp API
*/
@NonNull
public static LevelUpConnection newInstance(@NonNull final Context context) {
LevelUpConnection connection = null;
if (null != sNextInstance) {
// For Testing: if the client set a next instance, use that instead of new object
connection = sNextInstance;
} else {
connection = new LevelUpConnection(context);
}
return connection;
}
/**
* For Testing: set the next instance of LevelUpConnection that
* {@link #newInstance(Context)} will return. This instance will be returned for ALL SUBSEQUENT
* calls to {@link #newInstance(Context)} until it is cleared by calling
* {@link #setNextInstance(LevelUpConnection)} with null.
*
* @param connection the next LevelUpConnection to return. Setting {@code null}
* ensures that {@link #newInstance(Context)} will return a new instance.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
/* package */static void
setNextInstance(@Nullable final LevelUpConnection connection) {
sNextInstance = connection;
}
/**
* @param url the URL of the request, if null will return any request in the map if there are
* any.
* @throws AssertionError if a call is made with a null URL and there are multiple requests
* cached in the map.
* @return the last request made for the URL passed.
*/
@Nullable
@VisibleForTesting(visibility = Visibility.PRIVATE)
/* package */AbstractRequest getLastRequest(@Nullable final String url) {
synchronized (mRequestIntrinsicLock) {
LogManager.i("getLastRequest url=%s size=%d", url, mLastRequestMap.size());
AbstractRequest request = null;
String key = null;
if (null != url) {
key = url;
} else {
if (mLastRequestMap.size() > 1) {
throw new AssertionError(
"This method of getting last request is not supported if there are multiple requests being made");
}
if (mLastRequestMap.size() > 0) {
key = mLastRequestMap.keySet().iterator().next();
}
}
request = mLastRequestMap.get(key);
return request;
}
}
/**
* Testing Helper: set the last request for a URL.
*
* @param url the URL for this request
* @param request the last request that was made.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
/* package */void setLastRequest(@Nullable final String url,
@Nullable final AbstractRequest request) {
synchronized (mRequestIntrinsicLock) {
mLastRequestMap.put(url, request);
}
}
/**
* Testing Helper: set the next response to return for the URL passed.
*
* @param url the URL that this response should be returned for (null sets the response for any
* request).
* @param nextResponse the next response to return (null clears the response).
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
/* package */void setNextResponse(@Nullable final String url,
@Nullable final LevelUpResponse nextResponse) {
synchronized (mResponseIntrinsicLock) {
mNextResponseMap.put(url, nextResponse);
}
}
/**
* Testing Helper: get the next response to return for the URL passed.
*
* @param url the URL that this response should be returned for.
* @return the response to return next.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
/* package */LevelUpResponse getNextResponse(@NonNull final String url) {
LevelUpResponse response = null;
synchronized (mResponseIntrinsicLock) {
response = mNextResponseMap.get(url);
if (null == response) {
response = mNextResponseMap.get(null);
}
}
return response;
}
/**
* Testing Helper: set if un-caught (see {@link #setNextResponse}) network activity should be
* enabled. If disabled, any network activity that does not have a pre-created response will
* throw an exception.
*
* @param enabled true if network connections should be enabled.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
/* package */static void setNetworkEnabled(final boolean enabled) {
sIsNetworkEnabled = enabled;
}
/**
* Performs the request. Will add the headers to the request and build the full URL.
*
* @param request the {@link AbstractRequest} to send to the server.
* @return the parsed {@link LevelUpResponse} from the request.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
@NonNull
public LevelUpResponse send(@NonNull final AbstractRequest request) {
LevelUpResponse response = null;
String requestUrl = null;
LevelUpResponse nextResponse = null;
try {
requestUrl = request.getUrlString(mContext);
nextResponse = getNextResponse(requestUrl);
} catch (final BadRequestException e) {
LogManager.e("BadRequestException", e);
// Don't need to do anything, since this URL is just for logging.
}
// Set the last request for testing
setLastRequest(requestUrl, request);
LogManager.v("Requesting URL: %s %s", request.getMethod(), requestUrl);
if (null != nextResponse) {
LogManager.d("Returning canned response instead of performing network operation");
// TESTING: If the client set a next response, use that instead.
response = nextResponse;
setNextResponse(requestUrl, null);
} else {
if (!sIsNetworkEnabled) {
throw new RuntimeException(String.format(Locale.US,
"Network Activity detected when it was explicitly disabled: %s", requestUrl));
}
response = new LevelUpResponse(NetworkConnection.send(mContext, request));
}
return response;
}
}