/* DConnectProfile.java Copyright (c) 2014 NTT DOCOMO,INC. Released under the MIT license http://opensource.org/licenses/mit-license.php */ package org.deviceconnect.android.profile; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import org.deviceconnect.android.event.Event; import org.deviceconnect.android.localoauth.ClientPackageInfo; import org.deviceconnect.android.localoauth.LocalOAuth2Main; import org.deviceconnect.android.message.DConnectMessageService; import org.deviceconnect.android.message.MessageUtils; import org.deviceconnect.android.profile.api.DConnectApi; import org.deviceconnect.android.profile.spec.DConnectApiSpec; import org.deviceconnect.android.profile.spec.DConnectProfileSpec; import org.deviceconnect.android.profile.spec.DConnectSpecConstants; import org.deviceconnect.android.service.DConnectService; import org.deviceconnect.message.DConnectMessage; import org.deviceconnect.message.intent.message.IntentDConnectMessage; import org.deviceconnect.profile.DConnectProfileConstants; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Logger; /** * DConnect プロファイルクラス. * @author NTT DOCOMO, INC. */ public abstract class DConnectProfile implements DConnectProfileConstants, DConnectSpecConstants { /** バッファサイズを定義. */ private static final int BUF_SIZE = 4096; /** 内部エクストラ: {@value}. */ private static final String INNER_EXTRA_ORIGIN = "_origin"; /** * コンテキスト. */ private Context mContext; /** * DeviceConnectサービス. */ private DConnectService mService; /** * Device Connect API 仕様定義リスト. */ private DConnectProfileSpec mProfileSpec; /** * ロガー. */ protected final Logger mLogger = Logger.getLogger("org.deviceconnect.dplugin"); /** * サポートするAPI. */ protected final Map<ApiIdentifier, DConnectApi> mApis = new HashMap<ApiIdentifier, DConnectApi>(); protected boolean isEqual(final String s1, final String s2) { if (s1 == null && s2 == null) { return true; } if (s1 != null) { return s1.equalsIgnoreCase(s2); } else { return s2.equalsIgnoreCase(s1); } } /** * プロファイルに設定されているDevice Connect API実装のリストを返す. * * @return API実装のリスト */ public List<DConnectApi> getApiList() { List<DConnectApi> list = new ArrayList<DConnectApi>(); list.addAll(mApis.values()); return list; } /** * 指定されたリクエストに対応するDevice Connect API実装を返す. * * @param request リクエスト * @return 指定されたリクエストに対応するAPI実装を返す. 存在しない場合は<code>null</code> */ public DConnectApi findApi(final Intent request) { String action = request.getAction(); Method method = Method.fromAction(action); if (method == null) { return null; } if (mProfileSpec != null && mProfileSpec.getApiName() != null) { // XXXX パスの大文字小文字を無視 if (!mProfileSpec.getApiName().equalsIgnoreCase(getApi(request))) { return null; } } String path = getApiPath(request); return findApi(path, method); } /** * 指定されたリクエストに対応するDevice Connect API実装を返す. * * @param path リクエストされたAPIのパス * @param method リクエストされたAPIのメソッド * @return 指定されたリクエストに対応するAPI実装を返す. 存在しない場合は<code>null</code> */ public DConnectApi findApi(final String path, final Method method) { return mApis.get(new ApiIdentifier(path, method)); } /** * Device Connect API実装を追加する. * @param api API 追加するAPI実装 */ public void addApi(final DConnectApi api) { mApis.put(new ApiIdentifier(getApiPath(api), api.getMethod()), api); } /** * Device Connect API実装を削除する. * @param api 削除するAPI実装 */ public void removeApi(final DConnectApi api) { mApis.remove(new ApiIdentifier(getApiPath(api), api.getMethod())); } public boolean hasApi(final String path, final Method method) { return findApi(path, method) != null; } /** * 指定されたDevice Connect APIへのパスを返す. * @param api API実装 * @return パス */ private String getApiPath(final DConnectApi api) { return getApiPath(api.getInterface(), api.getAttribute()); } /** * リクエストで指定されたパスを返す. * @param request リクエスト * @return パス */ private String getApiPath(final Intent request) { return getApiPath(getInterface(request), getAttribute(request)); } /** * インターフェース名、アトリビュート名からパスを作成する. * @param interfaceName インターフェース名 * @param attributeName アトリビュート名 * @return パス */ private String getApiPath(final String interfaceName, final String attributeName) { StringBuilder path = new StringBuilder(); path.append("/"); if (interfaceName != null) { path.append(interfaceName); path.append("/"); } if (attributeName != null) { path.append(attributeName); } return path.toString(); } private boolean isKnownPath(final Intent request) { String path = getApiPath(request); if (mProfileSpec == null) { return false; } return mProfileSpec.findApiSpecs(path) != null; } private boolean isKnownMethod(final Intent request) { String action = request.getAction(); Method method = Method.fromAction(action); if (method == null) { return false; } String path = getApiPath(request); if (mProfileSpec == null) { return false; } return mProfileSpec.findApiSpec(path, method) != null; } /** * プロファイル名を取得する. * * @return プロファイル名 */ public abstract String getProfileName(); /** * RESPONSEメソッドハンドラー.<br> * リクエストされたAPIが実装されていて、かつ、パラメータが正常な場合は * {@link DConnectApi#onRequest(Intent, Intent)}を実行する. * そうでない場合は、即座にエラーレスポンスを送信する. * * @param request リクエストパラメータ * @param response レスポンスパラメータ * @return レスポンスパラメータを送信するか否か */ public boolean onRequest(final Intent request, final Intent response) { DConnectApi api = findApi(request); if (api != null) { DConnectApiSpec spec = api.getApiSpec(); if (spec != null) { if (!spec.validate(request)) { MessageUtils.setInvalidRequestParameterError(response); return true; } } return api.onRequest(request, response); } else { if (isKnownPath(request)) { if (isKnownMethod(request)) { MessageUtils.setNotSupportAttributeError(response); } else { MessageUtils.setNotSupportActionError(response); } } else { MessageUtils.setUnknownAttributeError(response); } return true; } } protected boolean isUseLocalOAuth() { return ((DConnectMessageService) getContext()).isUseLocalOAuth(); } protected boolean isIgnoredProfile(final String profileName) { return ((DConnectMessageService) getContext()).isIgnoredProfile(profileName); } private String findRequestOrigin(final String accessToken) { ClientPackageInfo info = LocalOAuth2Main.findClientPackageInfoByAccessToken(accessToken); return info.getPackageInfo().getPackageName(); } /** * コンテキストの設定する. * * @param context コンテキスト */ public void setContext(final Context context) { mContext = context; } /** * コンテキストの取得する. * * @return コンテキスト */ public Context getContext() { return mContext; } /** * 本プロファイル実装を提供するサービスを設定する. * * @param service サービス */ public void setService(final DConnectService service) { mService = service; } /** * 本プロファイル実装を提供するサービスを取得する. * * @return サービス */ public DConnectService getService() { return mService; } /** * Device Connect API 仕様定義リストを設定する. * @param profileSpec API 仕様定義リスト */ public void setProfileSpec(final DConnectProfileSpec profileSpec) { mProfileSpec = profileSpec; for (DConnectApi api : getApiList()) { String path = createPath(api); DConnectApiSpec spec = profileSpec.findApiSpec(path, api.getMethod()); if (spec != null) { api.setApiSpec(spec); } } } private String createPath(final DConnectApi api) { String interfaceName = api.getInterface(); String attributeName = api.getAttribute(); StringBuffer path = new StringBuffer(); path.append("/"); if (interfaceName != null) { path.append(interfaceName); path.append("/"); } if (attributeName != null) { path.append(attributeName); } return path.toString(); } /** * Device Connect API 仕様定義リストを取得する. * @return API 仕様定義リスト */ public DConnectProfileSpec getProfileSpec() { return mProfileSpec; } /** * 指定されたオブジェクトがStringか指定されたNumberクラスかを判定し、指定されたNumberクラスへ変換する. * * @param o 値 * @param clazz 型情報 * @param <T> ナンバークラスの型。判定出来るのは {@link Byte}、{@link Short}、{@link Integer}、 * {@link Long}、{@link Float}、{@link Double} のみ。 * @return 指定されたナンバークラスのオブジェクト。変換に失敗した場合はnullを返す。 */ @SuppressWarnings("unchecked") private static <T extends Number> Number valueOf(final Object o, final Class<T> clazz) { if (o == null) { return null; } Number result = null; if (o instanceof String) { try { if (Integer.class.equals(clazz)) { result = Integer.valueOf((String) o); } else if (Long.class.equals(clazz)) { result = Long.valueOf((String) o); } else if (Double.class.equals(clazz)) { result = Double.valueOf((String) o); } else if (Byte.class.equals(clazz)) { result = Byte.valueOf((String) o); } else if (Short.class.equals(clazz)) { result = Short.valueOf((String) o); } else if (Float.class.equals(clazz)) { result = Float.valueOf((String) o); } } catch (NumberFormatException e) { result = null; } } else if (o.getClass().equals(clazz)) { result = (T) o; } return result; } /** * 指定されたオブジェクトがStringかIntegerかを判定し、Integerへ変換する. * * @param o 値 * @return 変換後の値。変換に失敗した場合はnullを返す。 */ public static Integer parseInteger(final Object o) { Integer res = (Integer) valueOf(o, Integer.class); return res; } /** * Intentの指定されたパラメータがStringかIntegerかを判定し、Integerへ変換する. * * @param intent インテント * @param key パラメータキー * @return 変換後の値。変換に失敗した場合、またはパラメータが無い場合はnullを返す。 */ public static Integer parseInteger(final Intent intent, final String key) { Bundle b = intent.getExtras(); if (b == null) { return null; } Integer res = parseInteger(b.get(key)); return res; } /** * 指定されたオブジェクトがStringかLongかを判定し、Longへ変換する. * * @param o 値 * @return 変換後の値。変換に失敗した場合はnullを返す。 */ public static Long parseLong(final Object o) { Long res = (Long) valueOf(o, Long.class); return res; } /** * Intentの指定されたパラメータがStringかLongかを判定し、Longへ変換する. * * @param intent インテント * @param key パラメータキー * @return 変換後の値。変換に失敗した場合、またはパラメータが無い場合はnullを返す。 */ public static Long parseLong(final Intent intent, final String key) { Bundle b = intent.getExtras(); if (b == null) { return null; } Long res = parseLong(b.get(key)); return res; } /** * 指定されたオブジェクトがStringかDoubleかを判定し、Doubleへ変換する. * * @param o 値 * @return 変換後の値。変換に失敗した場合はnullを返す。 */ public static Double parseDouble(final Object o) { Double res = (Double) valueOf(o, Double.class); return res; } /** * Intentの指定されたパラメータがStringかDoubleかを判定し、Doubleへ変換する. * * @param intent インテント * @param key パラメータキー * @return 変換後の値。変換に失敗した場合、またはパラメータが無い場合はnullを返す。 */ public static Double parseDouble(final Intent intent, final String key) { Bundle b = intent.getExtras(); if (b == null) { return null; } Double res = parseDouble(b.get(key)); return res; } /** * 指定されたオブジェクトがStringかFloatかを判定し、Floatへ変換する. * * @param o 値 * @return 変換後の値。変換に失敗した場合はnullを返す。 */ public static Float parseFloat(final Object o) { Float res = (Float) valueOf(o, Float.class); return res; } /** * Intentの指定されたパラメータがStringかFloatかを判定し、Floatへ変換する. * * @param intent インテント * @param key パラメータキー * @return 変換後の値。変換に失敗した場合、またはパラメータが無い場合はnullを返す。 */ public static Float parseFloat(final Intent intent, final String key) { Bundle b = intent.getExtras(); if (b == null) { return null; } Float res = parseFloat(b.get(key)); return res; } /** * 指定されたオブジェクトがStringかByteかを判定し、Byteへ変換する. * * @param o 値 * @return 変換後の値。変換に失敗した場合はnullを返す。 */ public static Byte parseByte(final Object o) { Byte res = (Byte) valueOf(o, Byte.class); return res; } /** * Intentの指定されたパラメータがStringかByteかを判定し、Byteへ変換する. * * @param intent インテント * @param key パラメータキー * @return 変換後の値。変換に失敗した場合、またはパラメータが無い場合はnullを返す。 */ public static Byte parseByte(final Intent intent, final String key) { Bundle b = intent.getExtras(); if (b == null) { return null; } Byte res = parseByte(b.get(key)); return res; } /** * 指定されたオブジェクトがStringかShortかを判定し、Shortへ変換する. * * @param o 値 * @return 変換後の値。変換に失敗した場合はnullを返す。 */ public static Short parseShort(final Object o) { Short res = (Short) valueOf(o, Short.class); return res; } /** * Intentの指定されたパラメータがStringかShortかを判定し、Shortへ変換する. * * @param intent インテント * @param key パラメータキー * @return 変換後の値。変換に失敗した場合、またはパラメータが無い場合はnullを返す。 */ public static Short parseShort(final Intent intent, final String key) { Bundle b = intent.getExtras(); if (b == null) { return null; } Short res = parseShort(b.get(key)); return res; } /** * 指定されたオブジェクトがStringかBooleanかを判定し、Booleanへ変換する. * Stringの場合は、"true"の場合true、"false"の場合falseを返す。その他はnullを返す。 * * @param o 値 * @return 変換後の値。変換に失敗した場合はnullを返す。 */ public static Boolean parseBoolean(final Object o) { if (o instanceof String) { if (o.equals("true")) { return Boolean.TRUE; } else if (o.equals("false")) { return Boolean.FALSE; } } else if (o instanceof Boolean) { return (Boolean) o; } return null; } /** * Intentの指定されたパラメータがStringかBooleanかを判定し、Booleanへ変換する. * Stringの場合は、"true"の場合true、"false"の場合falseを返す。その他はnullを返す。 * * @param intent インテント * @param key キー * @return 変換後の値。変換に失敗した場合、またはパラメータが無い場合はnullを返す。 */ public static Boolean parseBoolean(final Intent intent, final String key) { Bundle b = intent.getExtras(); if (b == null) { return null; } return parseBoolean(b.get(key)); } /** * リクエストからサービスIDを取得する. * * @param request リクエストパラメータ * @return サービスID。無い場合はnullを返す。 */ public static String getServiceID(final Intent request) { String serviceId = request.getStringExtra(PARAM_SERVICE_ID); return serviceId; } /** * メッセージにサービスIDを設定する. * * @param message メッセージパラメータ * @param serviceId サービスID */ public static void setServiceID(final Intent message, final String serviceId) { message.putExtra(PARAM_SERVICE_ID, serviceId); } /** * リクエストからAPI名を取得する. * * @param request リクエストパラメータ * @return API名。無い場合はnullを返す。 */ public static String getApi(final Intent request) { String api = request.getStringExtra(DConnectMessage.EXTRA_API); return api; } /** * メッセージにAPI名を設定する. * * @param message メッセージパラメータ * @param api API名 */ public static void setApi(final Intent message, final String api) { message.putExtra(DConnectMessage.EXTRA_API, api); } /** * リクエストからプロファイル名を取得する. * * @param request リクエストパラメータ * @return プロファイル名。無い場合はnullを返す。 */ public static String getProfile(final Intent request) { String profile = request.getExtras().getString(DConnectMessage.EXTRA_PROFILE); return profile; } /** * メッセージにプロファイル名を設定する. * * @param message メッセージパラメータ * @param profile プロファイル名 */ public static void setProfile(final Intent message, final String profile) { message.putExtra(DConnectMessage.EXTRA_PROFILE, profile); } /** * リクエストからインターフェース名を取得する. * * @param request リクエストパラメータsetProfile * @return インターフェース。無い場合はnullを返す。 */ public static String getInterface(final Intent request) { String inter = request.getExtras().getString(DConnectMessage.EXTRA_INTERFACE); return inter; } /** * メッセージにインターフェース名を設定する. * * @param message メッセージパラメータ * @param inter インターフェース名 */ public static void setInterface(final Intent message, final String inter) { message.putExtra(DConnectMessage.EXTRA_INTERFACE, inter); } /** * リクエストから属性名を取得する. * * @param request リクエストパラメータ * @return 属性名。無い場合はnullを返す。 */ public static String getAttribute(final Intent request) { String attribute = request.getExtras().getString(DConnectMessage.EXTRA_ATTRIBUTE); return attribute; } /** * メッセージに属性名を設定する. * * @param message メッセージパラメータ * @param attribute コールバック名 */ public static void setAttribute(final Intent message, final String attribute) { message.putExtra(DConnectMessage.EXTRA_ATTRIBUTE, attribute); } /** * レスポンス結果を設定する. * * @param response レスポンスパラメータ * @param result レスポンス結果 */ public static void setResult(final Intent response, final int result) { response.putExtra(DConnectMessage.EXTRA_RESULT, result); } /** * レスポンス結果を取得する. * * @param response レスポンスパラメータ * @return レスポンス結果 */ public static int getResult(final Intent response) { int result = response.getIntExtra(DConnectMessage.EXTRA_RESULT, -1); return result; } /** * リクエストからオリジンを取得する. * * @param request リクエストパラメータ * @return オリジン。無い場合はnullを返す。 */ public static String getOrigin(final Intent request) { return request.getStringExtra(IntentDConnectMessage.EXTRA_ORIGIN); } /** * リクエストからセッションキーを取得する. * * @param request リクエストパラメータ * @return セッションキー。無い場合はnullを返す。 * @deprecated GotAPI 1.1では、セッションキーではなくオリジンごとにイベントを管理 * @see {@link #getOrigin(Intent)} */ public static String getSessionKey(final Intent request) { return request.getStringExtra(PARAM_SESSION_KEY); } /** * メッセージにセッションキーを設定する. * * @param message メッセージパラメータ * @param sessionKey セッションキー * @deprecated GotAPI 1.1では、セッションキーではなくオリジンごとにイベントを管理 */ public static void setSessionKey(final Intent message, final String sessionKey) { message.putExtra(PARAM_SESSION_KEY, sessionKey); } /** * リクエストからアクセストークンを取得する. * * @param request リクエストパラメータ * @return アクセストークン。無い場合はnullを返す。 */ public static String getAccessToken(final Intent request) { String accessToken = request.getStringExtra(DConnectMessage.EXTRA_ACCESS_TOKEN); return accessToken; } /** * メッセージにアクセストークンを設定する. * * @param message メッセージパラメータ * @param accessToken アクセストークン */ public static void setAccessToken(final Intent message, final String accessToken) { message.putExtra(DConnectMessage.EXTRA_ACCESS_TOKEN, accessToken); } /** * リクエストからDeviceConnectManagerのバージョン名を取得する. * * @param request リクエストパラメータ * @return DeviceConnectManagerのバージョン名。無い場合はnullを返す。 */ public static String getVersion(final Intent request) { String version = request.getStringExtra(DConnectMessage.EXTRA_VERSION); return version; } /** * リクエストからDeviceConnectManagerのバージョン名を設定する. * * @param message メッセージパラメータ * @param version DeviceConnectManagerのバージョン名 */ public static void setVersion(final Intent message, final String version) { message.putExtra(DConnectMessage.EXTRA_VERSION, version); } /** * リクエストからDeviceConnectManagerのアプリ名を取得する. * * @param request リクエストパラメータ * @return DeviceConnectManagerのアプリ名。無い場合はnullを返す。 */ public static String getProduct(final Intent request) { String product = request.getStringExtra(DConnectMessage.EXTRA_PRODUCT); return product; } /** * リクエストからDeviceConnectManagerのアプリ名を設定する. * * @param message メッセージパラメータ * @param product DeviceConnectManagerのアプリ名 */ public static void setProduct(final Intent message, final String product) { message.putExtra(DConnectMessage.EXTRA_PRODUCT, product); } /** * レスポンスの結果として非サポートエラーを設定する. * * @param response レスポンスパラメータ */ public static void setUnsupportedError(final Intent response) { MessageUtils.setNotSupportAttributeError(response); } /** * レスポンスにリクエストコードを設定する. * * @param response レスポンスパラメータ * @param requestCode リクエストコード */ public static void setRequestCode(final Intent response, final int requestCode) { response.putExtra(DConnectMessage.EXTRA_REQUEST_CODE, requestCode); } /** * リクエストからリクエストコードを取得する. * * @param request リクエストパラメータ * @return リクエストコード */ public static int getRequestCode(final Intent request) { return request.getIntExtra(DConnectMessage.EXTRA_REQUEST_CODE, Integer.MIN_VALUE); } /** * レスポンスを返却します. * @param response レスポンス */ protected final void sendResponse(final Intent response) { ((DConnectMessageService) getContext()).sendResponse(response); } /** * イベントを送信します. * @param event イベント * @param accessToken アクセストークン * @return 送信成功の場合true、アクセストークンエラーの場合はfalseを返す。 */ protected final boolean sendEvent(final Intent event, final String accessToken) { return ((DConnectMessageService) getContext()).sendEvent(event, accessToken); } /** * イベントを送信します. * @param event イベント * @param bundle パラメータ * @return 送信成功の場合true、アクセストークンエラーの場合はfalseを返す。 */ protected final boolean sendEvent(final Event event, final Bundle bundle) { return ((DConnectMessageService) getContext()).sendEvent(event, bundle); } /** * コンテンツデータを取得する. * * @param uri URI * @return コンテンツデータ */ protected final byte[] getContentData(final String uri) { if (uri == null) { return null; } ByteArrayOutputStream out = new ByteArrayOutputStream(); InputStream in = null; byte[] buf = new byte[BUF_SIZE]; int len; try { ContentResolver r = getContext().getContentResolver(); in = r.openInputStream(Uri.parse(uri)); while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } return out.toByteArray(); } catch (OutOfMemoryError e) { return null; } catch (IOException e) { return null; } finally { if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } } } protected byte[] getData(String uri) throws OutOfMemoryError { HttpURLConnection connection = null; InputStream inputStream = null; byte[] data = null; try { URL url = new URL(uri); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.connect(); inputStream = connection.getInputStream(); data = readAll(inputStream); } catch (OutOfMemoryError e) { throw new OutOfMemoryError(e.getMessage()); } catch (IOException e) { e.printStackTrace(); } finally { if (connection != null) { connection.disconnect(); } try { if (inputStream != null) { inputStream.close(); } } catch (IOException exception) { exception.printStackTrace(); } } return data; } private byte[] readAll(InputStream inputStream) throws IOException { ByteArrayOutputStream bout = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; while (true) { int len = inputStream.read(buffer); if (len < 0) { break; } bout.write(buffer, 0, len); } return bout.toByteArray(); } private static class ApiIdentifier { private final String mPath; private final DConnectApiSpec.Method mMethod; public ApiIdentifier(final String path, final DConnectApiSpec.Method method) { if (path == null) { throw new IllegalArgumentException("path is null."); } if (method == null) { throw new IllegalArgumentException("method is null."); } mPath = path; mMethod = method; } public ApiIdentifier(final String path, final String method) { this(path, DConnectApiSpec.Method.parse(method)); } @Override public int hashCode() { int result = mPath.toLowerCase().hashCode(); // XXXX パスの大文字小文字を無視 result = 31 * result + mMethod.hashCode(); return result; } @Override public boolean equals(final Object o) { if (this == o) { return true; } if (!(o instanceof ApiIdentifier)) { return false; } ApiIdentifier that = ((ApiIdentifier) o); // XXXX パスの大文字小文字を無視 return mPath.equalsIgnoreCase(that.mPath) && mMethod == that.mMethod; } } }