/*
LocalOAuthRequest.java
Copyright (c) 2014 NTT DOCOMO,INC.
Released under the MIT license
http://opensource.org/licenses/mit-license.php
*/
package org.deviceconnect.android.manager.request;
import android.content.Intent;
import org.deviceconnect.android.manager.DConnectLocalOAuth;
import org.deviceconnect.android.manager.DConnectLocalOAuth.OAuthData;
import org.deviceconnect.android.manager.DevicePlugin;
import org.deviceconnect.android.manager.R;
import org.deviceconnect.android.message.MessageUtils;
import org.deviceconnect.message.DConnectMessage;
import org.deviceconnect.message.intent.message.IntentDConnectMessage;
import org.deviceconnect.profile.AuthorizationProfileConstants;
import org.deviceconnect.profile.DConnectProfileConstants;
import java.util.List;
import java.util.UUID;
import java.util.logging.Logger;
/**
* LocalOAuthを行うためのリクエスト.
* @author NTT DOCOMO, INC.
*/
public abstract class LocalOAuthRequest extends DConnectRequest {
/** プラグイン側のAuthorizationのアトリビュート名: {@value}. */
private static final String ATTRIBUTE_CREATE_CLIENT = "createClient";
/** プラグイン側のAuthorizationのアトリビュート名: {@value}. */
private static final String ATTRIBUTE_REQUEST_ACCESS_TOKEN = "requestAccessToken";
/** リトライ回数の最大値を定義. */
protected static final int MAX_RETRY_COUNT = 3;
/** ロガー. */
private final Logger mLogger = Logger.getLogger("dconnect.manager");
/** 送信先のデバイスプラグイン. */
protected DevicePlugin mDevicePlugin;
/** Local OAuthを使用するクラス. */
protected DConnectLocalOAuth mLocalOAuth;
/** ロックオブジェクト. */
protected final Object mLockObj = new Object();
/** リクエストコード. */
protected int mRequestCode;
/** アクセストークンの使用フラグ. */
protected boolean mUseAccessToken;
/** オリジン有効フラグ. */
protected boolean mRequireOrigin;
/** リトライ回数. */
protected int mRetryCount;
/**
* 送信先のデバイスプラグインを設定する.
* @param plugin デバイスプラグイン
*/
public void setDestination(final DevicePlugin plugin) {
mDevicePlugin = plugin;
}
/**
* Local OAuth管理クラスを設定する.
* @param auth Local OAuth管理クラス
*/
public void setLocalOAuth(final DConnectLocalOAuth auth) {
mLocalOAuth = auth;
}
/**
* アクセストークンの使用フラグを設定する.
* @param useAccessToken 使用する場合はtrue、それ以外はfalse
*/
public void setUseAccessToken(final boolean useAccessToken) {
mUseAccessToken = useAccessToken;
}
/**
* オリジン有効フラグを設定する.
* @param requireOrigin 有効にする場合はtrue、それ以外はfalse
*/
public void setRequireOrigin(final boolean requireOrigin) {
mRequireOrigin = requireOrigin;
}
/**
* アクセストークンの使用フラグを取得する.
* @return アクセストークンを使用する場合はtrue、それ以外はfalse
*/
public boolean isUseAccessToken() {
return mUseAccessToken;
}
@Override
public void setResponse(final Intent response) {
super.setResponse(response);
synchronized (mLockObj) {
mLockObj.notifyAll();
}
}
@Override
public boolean hasRequestCode(final int requestCode) {
return mRequestCode == requestCode;
}
@Override
public void run() {
if (mRequest == null) {
throw new RuntimeException("mRequest is null.");
}
if (mDevicePlugin == null) {
throw new RuntimeException("mDevicePlugin is null.");
}
// リトライ回数を定義
mRetryCount = 0;
// リクエストコードを作成する
mRequestCode = UUID.randomUUID().hashCode();
// 実行
executeRequest();
}
/**
* クライアントの作成をデバイスプラグインに要求する.
*
* 結果が返ってくるまで、この関数は返り値を返却しない。
* 返り値がnullの場合には、クライアントの作成に失敗している。
*
* [実装要求]
* null(エラー)を返す場合には、リクエスト元にレスポンスを返却するので注意が必要。
*
* @param serviceId サービスID
* @return クライアントデータ
*/
protected ClientData executeCreateClient(final String serviceId) {
// 命令を実行する前にレスポンスを初期化しておく
mResponse = null;
// 各デバイスに送信するリクエストを作成
Intent request = createRequestMessage(mRequest, mDevicePlugin);
request.setAction(IntentDConnectMessage.ACTION_GET);
request.setComponent(mDevicePlugin.getComponentName());
request.putExtra(IntentDConnectMessage.EXTRA_REQUEST_CODE, mRequestCode);
request.putExtra(DConnectMessage.EXTRA_PROFILE, AuthorizationProfileConstants.PROFILE_NAME);
request.putExtra(DConnectMessage.EXTRA_INTERFACE, (String) null);
request.putExtra(DConnectMessage.EXTRA_ATTRIBUTE, ATTRIBUTE_CREATE_CLIENT);
request.putExtra(DConnectProfileConstants.PARAM_SERVICE_ID, serviceId);
String origin = getRequestOrigin(mRequest);
request.putExtra(AuthorizationProfileConstants.PARAM_PACKAGE, origin);
// デバイスプラグインに送信
mContext.sendBroadcast(request);
if (mResponse == null) {
// 各デバイスのレスポンスを待つ
waitForResponse();
}
// レスポンスを返却する
if (mResponse != null) {
int result = getResult(mResponse);
if (result == DConnectMessage.RESULT_OK) {
String clientId = mResponse.getStringExtra(AuthorizationProfileConstants.PARAM_CLIENT_ID);
if (clientId == null) {
// クライアントの作成にエラー
sendCannotCreateClient();
} else {
// クライアントデータを
ClientData client = new ClientData();
client.mClientId = clientId;
client.mClientSecret = null;
return client;
}
} else {
int errorCode = getErrorCode(mResponse);
if (errorCode == DConnectMessage.ErrorCode.NOT_SUPPORT_PROFILE.getCode()) {
// authorizationプロファイルに対応していないのでアクセストークンはいらない。
mLogger.info("DevicePlugin not support Authorization Profile.");
executeRequest(null);
} else {
sendResponse(mResponse);
}
}
} else {
sendTimeout();
}
return null;
}
/**
* アクセストークンの取得要求をデバイスプラグインに対して行う.
*
* 結果が返ってくるまで、この関数は返り値を返却しない。
* アクセストークンの取得に失敗した場合にはnullを返却する。
*
* [実装要求]
* null(エラー)を返す場合には、リクエスト元にレスポンスを返却するので注意が必要。
*
* @param serviceId サービスID
* @param clientId クライアントID
* @return アクセストークン
*/
protected String executeAccessToken(final String serviceId, final String clientId) {
// 命令を実行する前にレスポンスを初期化しておく
mResponse = null;
// 各デバイスに送信するリクエストを作成
Intent request = createRequestMessage(mRequest, mDevicePlugin);
request.setAction(IntentDConnectMessage.ACTION_GET);
request.setComponent(mDevicePlugin.getComponentName());
request.putExtra(IntentDConnectMessage.EXTRA_REQUEST_CODE, mRequestCode);
request.putExtra(DConnectMessage.EXTRA_PROFILE, AuthorizationProfileConstants.PROFILE_NAME);
request.putExtra(DConnectMessage.EXTRA_INTERFACE, (String) null);
request.putExtra(DConnectMessage.EXTRA_ATTRIBUTE, ATTRIBUTE_REQUEST_ACCESS_TOKEN);
request.putExtra(AuthorizationProfileConstants.PARAM_CLIENT_ID, clientId);
request.putExtra(AuthorizationProfileConstants.PARAM_APPLICATION_NAME, mContext.getString(R.string.app_name));
request.putExtra(AuthorizationProfileConstants.PARAM_SCOPE, combineStr(getScope()));
// トークン取得を行う
mContext.sendBroadcast(request);
if (mResponse == null) {
// 各デバイスのレスポンスを待つ
waitForResponse();
}
// レスポンスを返却する
if (mResponse != null) {
int result = getResult(mResponse);
if (result == DConnectMessage.RESULT_OK) {
String accessToken = mResponse.getStringExtra(DConnectMessage.EXTRA_ACCESS_TOKEN);
if (accessToken == null) {
sendCannotCreateAccessToken();
} else {
return accessToken;
}
} else {
// 認証エラーで、有効期限切れ・スコープ範囲外以外はClientIdを作り直す処理を入れる
int errorCode = getErrorCode(mResponse);
if (errorCode == DConnectMessage.ErrorCode.NOT_FOUND_CLIENT_ID.getCode()
|| errorCode == DConnectMessage.ErrorCode.AUTHORIZATION.getCode()) {
mLocalOAuth.deleteOAuthData(getRequestOrigin(mRequest), serviceId);
}
sendResponse(mResponse);
}
} else {
sendTimeout();
}
return null;
}
/**
* 実際の命令を行う.
* @param accessToken アクセストークン
*/
protected abstract void executeRequest(final String accessToken);
/**
* resultの値をレスポンスのIntentから取得する.
* @param response レスポンスのIntent
* @return resultの値
*/
protected int getResult(final Intent response) {
int result = response.getIntExtra(DConnectMessage.EXTRA_RESULT,
DConnectMessage.RESULT_ERROR);
return result;
}
/**
* errorCodeの値をレスポンスのIntentから取得する.
* @param response レスポンスのIntent
* @return errorCodeの値
*/
protected int getErrorCode(final Intent response) {
int errorCode = response.getIntExtra(DConnectMessage.EXTRA_ERROR_CODE,
DConnectMessage.ErrorCode.UNKNOWN.getCode());
return errorCode;
}
/**
* 各デバイスからのレスポンスを待つ.
*
* この関数から返答があるのは以下の条件になる。
* <ul>
* <li>デバイスプラグインからレスポンスがあった場合
* <li>指定された時間無いにレスポンスが返ってこない場合
* </ul>
*/
protected void waitForResponse() {
synchronized (mLockObj) {
try {
mLockObj.wait(mTimeout);
} catch (InterruptedException e) {
return;
}
}
}
/**
* プラグインからアクセストークンを求められないプロファイルであるかどうかを判定する.
* @param profile プロファイル名
* @return アクセストークンを求めない場合は<code>true</code>、そうでなければ<code>false</code>
*/
private boolean isIgnoredPluginProfile(final String profile) {
for (String ignored : DConnectLocalOAuth.IGNORE_PLUGIN_PROFILES) {
if (ignored.equals(profile)) {
return true;
}
}
return false;
}
/**
* Local OAuthの有効期限切れの場合にリトライを行う.
*/
protected void executeRequest() {
String profile = mRequest.getStringExtra(DConnectMessage.EXTRA_PROFILE);
String serviceId = mRequest.getStringExtra(DConnectMessage.EXTRA_SERVICE_ID);
String origin = getRequestOrigin(mRequest);
if (mUseAccessToken && !isIgnoredPluginProfile(profile)) {
String accessToken = getAccessToken(origin, serviceId);
if (accessToken != null) {
executeRequest(accessToken);
} else {
// 認証を行うリクエスト
final OAuthRequest request = new OAuthRequest() {
@Override
public void onFinishAuth(final String accessToken) {
synchronized (this) {
this.notifyAll();
}
}
};
request.setContext(getContext());
request.setServiceId(serviceId);
request.setOrigin(origin);
// OAuthの認証だけは、シングルスレッドで動作させないとおかしな挙動が発生
mRequestMgr.addRequestOnSingleThread(request);
synchronized (request) {
try {
request.wait(mTimeout);
} catch (InterruptedException e) {
mLogger.warning("timeout.");
}
}
accessToken = getAccessToken(origin, serviceId);
if (accessToken != null) {
executeRequest(accessToken);
}
}
} else {
executeRequest(null);
}
}
private String getRequestOrigin(final Intent request) {
String origin = request.getStringExtra(IntentDConnectMessage.EXTRA_ORIGIN);
if (!mRequireOrigin && origin == null) {
origin = "<anonymous>";
}
return origin;
}
/**
* 指定されたサービスIDに対応するアクセストークンを取得する.
* アクセストークンが存在しない場合にはnullを返却する。
* @param origin リクエスト元のオリジン
* @param serviceId サービスID
* @return アクセストークン
*/
private String getAccessToken(final String origin, final String serviceId) {
OAuthData oauth = mLocalOAuth.getOAuthData(origin, serviceId);
if (oauth != null) {
return mLocalOAuth.getAccessToken(oauth.getId());
}
return null;
}
/**
* スコープを一つの文字列に連結する.
* @param scopes スコープ一覧
* @return 連結された文字列
*/
private String combineStr(final String[] scopes) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < scopes.length; i++) {
if (i > 0) {
builder.append(",");
}
builder.append(scopes[i].trim());
}
return builder.toString();
}
/**
* デバイスプラグインでサポートするプロファイルの一覧を取得する.
* @return プロファイルの一覧
*/
private String[] getScope() {
List<String> list = mDevicePlugin.getSupportProfiles();
return list.toArray(new String[list.size()]);
}
/**
* クライアントの作成に失敗した場合のレスポンスを返却する.
*/
private void sendCannotCreateClient() {
Intent response = new Intent(IntentDConnectMessage.ACTION_RESPONSE);
MessageUtils.setAuthorizationError(response, "Cannot create client data.");
sendResponse(response);
}
/**
* アクセストークンの作成に失敗した場合のレスポンスを返却する.
*/
private void sendCannotCreateAccessToken() {
Intent response = new Intent(IntentDConnectMessage.ACTION_RESPONSE);
MessageUtils.setAuthorizationError(response, "Cannot create access token.");
sendResponse(response);
}
/**
* クライアントデータ.
*/
protected class ClientData {
/** クライアントID. */
String mClientId;
/** クライアントシークレット. */
String mClientSecret;
}
/**
* Local OAuthの処理を行うリクエスト.
*/
private abstract class OAuthRequest extends DConnectRequest {
/** ロックオブジェクト. */
protected final Object mLockObj = new Object();
/** 送信元のオリジン. */
protected String mOrigin;
/** 送信先のサービスID. */
protected String mServiceId;
/**
* オリジンを設定する.
* @param origin オリジン
*/
public void setOrigin(final String origin) {
mOrigin = origin;
}
/**
* サービスIDを設定する.
* @param id サービスID
*/
public void setServiceId(final String id) {
mServiceId = id;
}
@Override
public void setResponse(final Intent response) {
super.setResponse(response);
synchronized (mLockObj) {
mLockObj.notifyAll();
}
}
@Override
public boolean hasRequestCode(final int requestCode) {
return false;
}
@Override
public void run() {
String clientId = null;
OAuthData oauth = mLocalOAuth.getOAuthData(mOrigin, mServiceId);
if (oauth == null) {
ClientData client = executeCreateClient(mServiceId);
if (client == null) {
// MEMO executeCreateClientの中でレスポンスは返しているので
// ここでは何も処理を行わない。
onFinishAuth(null);
return;
} else {
clientId = client.mClientId;
// クライアントデータを保存
mLocalOAuth.setOAuthData(mOrigin, mServiceId, clientId);
oauth = mLocalOAuth.getOAuthData(mOrigin, mServiceId);
}
} else {
clientId = oauth.getClientId();
}
String accessToken = mLocalOAuth.getAccessToken(oauth.getId());
if (accessToken == null) {
// 再度アクセストークンを取得してから再度実行
accessToken = executeAccessToken(mServiceId, clientId);
if (accessToken == null) {
// MEMO executeAccessTokenの中でレスポンスは返しているので
// ここでは何も処理を行わない。
onFinishAuth(null);
return;
} else {
// アクセストークンを保存
mLocalOAuth.setAccessToken(oauth.getId(), accessToken);
}
}
onFinishAuth(accessToken);
}
/**
* 認証完了通知用メソッド.
* <p>
* 認証が完了した場合に、このメソッドが呼び出される。
* 認証に成功した場合には、アクセストークンが渡される。
* 認証に失敗した場合にはnullが渡される。
* </p>
* @param accessToken アクセストークン
*/
public abstract void onFinishAuth(String accessToken);
};
}