diff options
Diffstat (limited to 'third_party/sl4a/src/main/java/com/google/android/mobly/snippet')
7 files changed, 305 insertions, 65 deletions
diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java index a1d3425..f543b62 100644 --- a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/JsonBuilder.java @@ -21,9 +21,6 @@ import android.content.Intent; import android.os.Bundle; import android.os.ParcelUuid; import com.google.android.mobly.snippet.manager.SnippetObjectConverterManager; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -34,11 +31,11 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +/** Builds the result for JSON RPC. */ public class JsonBuilder { private JsonBuilder() {} - @SuppressWarnings("unchecked") public static Object build(Object data) throws JSONException { if (data == null) { return JSONObject.NULL; @@ -89,30 +86,13 @@ public class JsonBuilder { } if (data instanceof Map<?, ?>) { // TODO(damonkohler): I would like to make this a checked cast if possible. - return buildJsonMap((Map<String, ?>) data); + return buildJsonMap((Map<?, ?>) data); } if (data instanceof ParcelUuid) { return data.toString(); } - // TODO(xpconanfan): Deprecate the following default non-primitive type builders. - if (data instanceof InetSocketAddress) { - return buildInetSocketAddress((InetSocketAddress) data); - } - if (data instanceof InetAddress) { - return buildInetAddress((InetAddress) data); - } - if (data instanceof URL) { - return buildURL((URL) data); - } - if (data instanceof byte[]) { - JSONArray result = new JSONArray(); - for (byte b : (byte[]) data) { - result.put(b & 0xFF); - } - return result; - } - if (data instanceof Object[]) { - return buildJSONArray((Object[]) data); + if (data.getClass().isArray()) { + return buildJSONArray(data); } // Try with custom converter provided by user. Object result = SnippetObjectConverterManager.getInstance().objectToJson(data); @@ -122,24 +102,44 @@ public class JsonBuilder { return data.toString(); } - private static Object buildInetAddress(InetAddress data) { - JSONArray address = new JSONArray(); - address.put(data.getHostName()); - address.put(data.getHostAddress()); - return address; - } - - private static Object buildInetSocketAddress(InetSocketAddress data) { - JSONArray address = new JSONArray(); - address.put(data.getHostName()); - address.put(data.getPort()); - return address; - } - - private static JSONArray buildJSONArray(Object[] data) throws JSONException { + private static JSONArray buildJSONArray(Object data) throws JSONException { JSONArray result = new JSONArray(); - for (Object o : data) { - result.put(build(o)); + if (data instanceof int[]) { + for (int i : (int []) data) { + result.put(i); + } + } else if (data instanceof short[]) { + for (short s : (short[]) data) { + result.put(s); + } + } else if (data instanceof long[]) { + for (long l : (long[]) data) { + result.put(l); + } + } else if (data instanceof float[]) { + for (float f : (float[]) data) { + result.put(f); + } + } else if (data instanceof double[]) { + for (double d : (double[]) data) { + result.put(d); + } + } else if (data instanceof boolean[]) { + for (boolean b : (boolean[]) data) { + result.put(b); + } + } else if (data instanceof char[]) { + for (char c : (char[]) data) { + result.put(c); + } + } else if (data instanceof byte[]) { + for (byte b : (byte[]) data) { + result.put(b & 0xFF); + } + } else { + for (Object o : (Object[]) data) { + result.put(build(o)); + } } return result; } @@ -177,25 +177,13 @@ public class JsonBuilder { return result; } - private static JSONObject buildJsonMap(Map<String, ?> map) throws JSONException { + private static JSONObject buildJsonMap(Map<?, ?> map) throws JSONException { JSONObject result = new JSONObject(); - for (Entry<String, ?> entry : map.entrySet()) { - String key = entry.getKey(); - if (key == null) { - key = ""; - } - result.put(key, build(entry.getValue())); + for (Entry<?, ?> entry : map.entrySet()) { + Object key = entry.getKey(); + String keyStr = key == null ? "" : key.toString(); + result.put(keyStr, build(entry.getValue())); } return result; } - - private static Object buildURL(URL data) throws JSONException { - JSONObject url = new JSONObject(); - url.put("Authority", data.getAuthority()); - url.put("Host", data.getHost()); - url.put("Path", data.getPath()); - url.put("Port", data.getPort()); - url.put("Protocol", data.getProtocol()); - return url; - } } diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java index b9c8a7a..214ffe7 100644 --- a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/MethodDescriptor.java @@ -22,18 +22,25 @@ import com.google.android.mobly.snippet.Snippet; import com.google.android.mobly.snippet.manager.SnippetManager; import com.google.android.mobly.snippet.manager.SnippetObjectConverterManager; import com.google.android.mobly.snippet.util.AndroidUtil; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; /** An adapter that wraps {@code Method}. */ public final class MethodDescriptor { + private static final Map<Class<?>, TypeConverter<?>> typeConverters = populateConverters(); + private final Method mMethod; private final Class<? extends Snippet> mClass; @@ -68,6 +75,7 @@ public final class MethodDescriptor { * @throws Throwable the exception raised from executing the RPC method. */ public Object invoke(SnippetManager manager, final JSONArray parameters) throws Throwable { + final Annotation[][] annotations = getParameterAnnotations(); final Type[] parameterTypes = getGenericParameterTypes(); final Object[] args = new Object[parameterTypes.length]; @@ -79,6 +87,12 @@ public final class MethodDescriptor { final Type parameterType = parameterTypes[i]; if (i < parameters.length()) { args[i] = convertParameter(parameters, i, parameterType); + } else if (MethodDescriptor.hasDefaultValue(Arrays.asList(annotations[i]))) { + args[i] = MethodDescriptor.getDefaultValue( + parameterType, Arrays.asList(annotations[i])); + } else if (MethodDescriptor.isOptional(Arrays.asList(annotations[i]))) { + args[i] = MethodDescriptor.getOptionalValue( + parameterType, Arrays.asList(annotations[i])); } else { throw new RpcError("Argument " + (i + 1) + " is not present"); } @@ -88,10 +102,6 @@ public final class MethodDescriptor { } /** Converts a parameter from JSON into a Java Object. */ - // TODO(damonkohler): This signature is a bit weird (auto-refactored). The obvious alternative - // would be to work on one supplied parameter and return the converted parameter. However, - // that's problematic because you lose the ability to call the getXXX methods on the JSON array. - // @VisibleForTesting private static Object convertParameter(final JSONArray parameters, int index, Type type) throws JSONException, RpcError { try { @@ -165,7 +175,8 @@ public final class MethodDescriptor { + " should be of type " + ((Class<?>) type).getSimpleName() + ", but is of type " - + parameters.get(index).getClass().getSimpleName()); + + parameters.get(index).getClass().getSimpleName(), + e); } } @@ -226,6 +237,11 @@ public final class MethodDescriptor { Rpc annotation = mMethod.getAnnotation(Rpc.class); return annotation.description(); } + + public Annotation[][] getParameterAnnotations() { + return mMethod.getParameterAnnotations(); + } + /** * Returns a human-readable help text for this RPC, based on annotations in the source code. * @@ -249,4 +265,140 @@ public final class MethodDescriptor { mMethod.getReturnType().getSimpleName(), getAnnotationDescription()); } + + /** + * Returns the default value for a parameter which has a default value. + * + * @param parameterType parameterType + * @param annotations annotations of the parameter + */ + public static Object getDefaultValue(Type parameterType, Iterable<Annotation> annotations) { + for (Annotation a : annotations) { + if (a instanceof RpcDefault) { + RpcDefault defaultAnnotation = (RpcDefault) a; + TypeConverter<?> converter = + converterFor(parameterType, defaultAnnotation.converter()); + return converter.convert(defaultAnnotation.value()); + } + } + throw new IllegalStateException("No default value for " + parameterType); + } + + /** + * Returns null for an optional parameter. + * + * @param parameterType parameterType + * @param annotations annotations of the parameter + */ + public static Object getOptionalValue(Type parameterType, Iterable<Annotation> annotations) { + for (Annotation a : annotations) { + if (a instanceof RpcOptional) { + return null; + } + } + throw new IllegalStateException("No default value for " + parameterType); + } + + @SuppressWarnings("rawtypes") + private static TypeConverter<?> converterFor( + Type parameterType, Class<? extends TypeConverter> converterClass) { + if (converterClass == TypeConverter.class) { + TypeConverter<?> converter = typeConverters.get(parameterType); + if (converter == null) { + throw new IllegalArgumentException( + String.format("No predefined converter found for %s", parameterType)); + } + return converter; + } + try { + Constructor<?> constructor = converterClass.getConstructor(new Class<?>[0]); + return (TypeConverter<?>) constructor.newInstance(new Object[0]); + } catch (Exception e) { + throw new IllegalArgumentException( + String.format( + "Cannot create converter from %s", converterClass.getCanonicalName()), + e); + } + } + + /** + * Determines whether or not this parameter has default value. + * + * @param annotations annotations of the parameter + */ + public static boolean hasDefaultValue(Iterable<Annotation> annotations) { + for (Annotation a : annotations) { + if (a instanceof RpcDefault) { + return true; + } + } + return false; + } + + /** + * Determines whether or not this parameter is optional. + * + * @param annotations annotations of the parameter + */ + public static boolean isOptional(Iterable<Annotation> annotations) { + for (Annotation a : annotations) { + if (a instanceof RpcOptional) { + return true; + } + } + return false; + } + + /** + * Returns the converters for {@code String}, {@code Integer}, {@code Long}, + * and {@code Boolean}. + */ + private static Map<Class<?>, TypeConverter<?>> populateConverters() { + Map<Class<?>, TypeConverter<?>> converters = new HashMap<>(); + converters.put(String.class, new TypeConverter<String>() { + @Override + public String convert(String value) { + return value; + } + }); + converters.put(Integer.class, new TypeConverter<Integer>() { + @Override + public Integer convert(String input) { + try { + return Integer.decode(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + String.format("'%s' is not a Integer", input), e); + } + } + }); + converters.put(Long.class, new TypeConverter<Long>() { + @Override + public Long convert(String input) { + try { + return Long.decode(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + String.format("'%s' is not a Long", input), e); + } + } + }); + converters.put(Boolean.class, new TypeConverter<Boolean>() { + @Override + public Boolean convert(String input) { + if (input == null) { + return null; + } + input = input.toLowerCase(Locale.ROOT); + if (input.equals("true")) { + return Boolean.TRUE; + } + if (input.equals("false")) { + return Boolean.FALSE; + } + throw new IllegalArgumentException(String.format("'%s' is not a Boolean", input)); + } + }); + return converters; + } } diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcDefault.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcDefault.java new file mode 100644 index 0000000..bc98f7e --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcDefault.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * 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.google.android.mobly.snippet.rpc; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Use this annotation to mark an RPC parameter that have a default value. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +@Documented +public @interface RpcDefault { + /** The default value of the RPC parameter. */ + String value(); + + @SuppressWarnings("rawtypes") + Class<? extends TypeConverter> converter() default TypeConverter.class; +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java index 0862673..03038ee 100644 --- a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcError.java @@ -22,4 +22,8 @@ public class RpcError extends Exception { public RpcError(String message) { super(message); } + + public RpcError(String message, Throwable cause) { + super(message, cause); + } } diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcOptional.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcOptional.java new file mode 100644 index 0000000..b4b43aa --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/RpcOptional.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * 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.google.android.mobly.snippet.rpc; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Use this annotation to mark RPC parameter as optional. + * + * <p>The parameter marked as optional has no explicit default value. {@code null} is used as + * default value. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +@Documented +public @interface RpcOptional {} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/TypeConverter.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/TypeConverter.java new file mode 100644 index 0000000..396e526 --- /dev/null +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/rpc/TypeConverter.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * 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.google.android.mobly.snippet.rpc; + +/** + * A converter can take a String and turn it into an instance of type T (the type parameter to the + * converter). + */ +public interface TypeConverter<T> { + + /** Convert a string into type T. */ + T convert(String value); +} diff --git a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.java b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.java index 46c4940..69d5cf6 100644 --- a/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.java +++ b/third_party/sl4a/src/main/java/com/google/android/mobly/snippet/util/AndroidUtil.java @@ -25,8 +25,6 @@ import org.json.JSONObject; public final class AndroidUtil { private AndroidUtil() {} - // TODO(damonkohler): Pull this out into proper argument deserialization and support - // complex/nested types being passed in. public static void putExtrasFromJsonObject(JSONObject extras, Intent intent) throws JSONException { JSONArray names = extras.names(); |