myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");
-
- myList.stream()
- // .filter(s -> s.startsWith("c"))
- .map(String::toUpperCase).map(s -> s + "blah").map(s -> s + "oink").sorted().forEach(System.out::println);
-
- ApiSwagger swagger = new ApiSwagger();
-
- log.info("{}", CodecUtils.toJson(swagger.getSwagger()));
+
+ // log.info("{}", CodecUtils.toJson(ApiSwagger.generateSwaggerYaml(Servo.class)));
+ log.info("{}", ApiSwagger.generateSwaggerYaml(Servo.class));
} catch (Exception e) {
log.error("main threw", e);
}
}
- public Object process(MessageSender webgui, String apiKey, String uri, String uuid, OutputStream out, String json) throws Exception {
- // TODO Auto-generated method stub
- return null;
- }
-
}
diff --git a/src/main/java/org/myrobotlab/codec/CodecUtils.java b/src/main/java/org/myrobotlab/codec/CodecUtils.java
index 6d03fc4985..457f3a8fe5 100644
--- a/src/main/java/org/myrobotlab/codec/CodecUtils.java
+++ b/src/main/java/org/myrobotlab/codec/CodecUtils.java
@@ -11,11 +11,13 @@
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -26,14 +28,15 @@
import java.util.Set;
import java.util.stream.Collectors;
-import org.myrobotlab.codec.json.GsonPolymorphicTypeAdapterFactory;
import org.myrobotlab.codec.json.JacksonPolymorphicModule;
import org.myrobotlab.codec.json.JacksonPrettyPrinter;
import org.myrobotlab.codec.json.JsonDeserializationException;
import org.myrobotlab.codec.json.JsonSerializationException;
+import org.myrobotlab.codec.json.ProxySerializer;
import org.myrobotlab.framework.MRLListener;
import org.myrobotlab.framework.Message;
import org.myrobotlab.framework.MethodCache;
+import org.myrobotlab.framework.Platform;
import org.myrobotlab.framework.StaticType;
import org.myrobotlab.framework.interfaces.ServiceInterface;
import org.myrobotlab.logging.Level;
@@ -55,1451 +58,1496 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.module.noctordeser.NoCtorDeserModule;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.internal.LinkedTreeMap;
/**
* handles all encoding and decoding of MRL messages or api(s) assumed context -
* services can add an assumed context as a prefix
* /api/returnEncoding/inputEncoding/service/method/param1/param2/ ...
*
- * xmpp for example assumes (/api/string/gson)/service/method/param1/param2/ ...
+ * xmpp for example assumes (/api/string/json)/service/method/param1/param2/ ...
*
* scheme = alpha *( alpha | digit | "+" | "-" | "." ) Components of all URIs: [
* <scheme>:]<scheme-specific-part>[#<fragment>]
*
* branch API test 5
*
- * @see Valid characters for URI schemes
+ * @see Valid
+ * characters for URI schemes
*/
public class CodecUtils {
- public final static Logger log = LoggerFactory.getLogger(CodecUtils.class);
- /**
- * The string to be used to specify the API in URIs,
- * with leading and trailing slash.
- */
- public final static String PARAMETER_API = "/api/";
- /**
- * The string to be used in URIs to specify the API
- */
- public final static String PREFIX_API = "api";
- /**
- * The MIME type used to specify JSON data.
- *
- * @see MIME types
- */
- public final static String MIME_TYPE_JSON = "application/json";
+ public final static Logger log = LoggerFactory.getLogger(CodecUtils.class);
+ /**
+ * The string to be used to specify the API in URIs, with leading and trailing
+ * slash.
+ */
+ public final static String PARAMETER_API = "/api/";
+ /**
+ * The string to be used in URIs to specify the API
+ */
+ public final static String PREFIX_API = "api";
+ /**
+ * The MIME type used to specify JSON data.
+ *
+ * @see MIME
+ * types
+ */
+ public final static String MIME_TYPE_JSON = "application/json";
- // mime-types
- /**
- * Whether we are using the GSON JSON backend or not. If false,
- * use Jackson.
- * TODO Replace with enum to allow extension for multiple backends
- */
- public static final boolean USING_GSON = false;
- /**
- * The key used to locate type information
- * in a JSON dictionary. This is used to serialize
- * type information into the JSON and to deserialize
- * JSON into the correct type.
- */
- public static final String CLASS_META_KEY = "class";
- /**
- * Set of all known wrapper types, which are classes that correspond to
- * Java primitives (plus {@link Void}).
- *
- * @see Java Wrapper Classes
- */
- public static final Set> WRAPPER_TYPES = new HashSet<>(
- Arrays.asList(Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class,
- Float.class, Double.class, Void.class));
- /**
- * Set of the fully-qualified (AKA canonical) names of {@link #WRAPPER_TYPES}.
- *
- * @see Java Wrapper Classes
- */
- public static final Set WRAPPER_TYPES_CANONICAL =
- WRAPPER_TYPES.stream().map(Object::getClass).map(Class::getCanonicalName).collect(Collectors.toSet());
- public static final String API_MESSAGES = "messages";
- public static final String API_SERVICE = "service";
+ /**
+ * The key used to locate type information in a JSON dictionary. This is used
+ * to serialize type information into the JSON and to deserialize JSON into
+ * the correct type.
+ */
+ public static final String CLASS_META_KEY = "class";
+ /**
+ * Set of all known wrapper types, which are classes that correspond to Java
+ * primitives (plus {@link Void}).
+ *
+ * @see Java
+ * Wrapper Classes
+ */
+ public static final Set> WRAPPER_TYPES = new HashSet<>(
+ Arrays.asList(Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Void.class));
+ /**
+ * Set of the fully-qualified (AKA canonical) names of {@link #WRAPPER_TYPES}.
+ *
+ * @see Java
+ * Wrapper Classes
+ */
+ public static final Set WRAPPER_TYPES_CANONICAL = WRAPPER_TYPES.stream().map(Object::getClass).map(Class::getCanonicalName).collect(Collectors.toSet());
+ public static final String API_MESSAGES = "messages";
+ public static final String API_SERVICE = "service";
- /**
- * The path from a top-level URL to the messages API
- * endpoint.
- *
- * FIXME This should be moved to WebGui,
- * CodecUtils should have no knowledge of URLs
- */
- public static final String API_MESSAGES_PATH = PARAMETER_API + API_MESSAGES;
+ /**
+ * The path from a top-level URL to the messages API endpoint.
+ *
+ *
+ * FIXME This should be moved to WebGui, CodecUtils should have no knowledge
+ * of URLs
+ */
+ public static final String API_MESSAGES_PATH = PARAMETER_API + API_MESSAGES;
+ /**
+ * The path from a top-level URL to the service API endpoint.
+ *
+ *
+ * FIXME This should be moved to WebGui, CodecUtils should have no knowledge
+ * of URLs
+ */
+ public static final String API_SERVICE_PATH = PARAMETER_API + API_SERVICE;
+ /**
+ * use {@link MethodCache}
+ */
+ @Deprecated
+ final static HashMap methodCache = new HashMap();
+ /**
+ * a method signature map based on name and number of methods - the String[]
+ * will be the keys into the methodCache A method key is generated by input
+ * from some encoded protocol - the method key is object name + method name +
+ * parameter number - this returns a full method signature key which is used
+ * to look up the method in the methodCache
+ */
+ final static HashMap> methodOrdinal = new HashMap>();
+ final static HashSet objectsCached = new HashSet();
+ /**
+ * Equivalent to {@link #MIME_TYPE_JSON}
+ */
+ @Deprecated
+ static final String JSON = "application/javascript";
- /**
- * The path from a top-level URL to the service API
- * endpoint.
- *
- * FIXME This should be moved to WebGui,
- * CodecUtils should have no knowledge of URLs
- */
- public static final String API_SERVICE_PATH = PARAMETER_API + API_SERVICE;
- /**
- * use {@link MethodCache}
- */
- @Deprecated
- final static HashMap methodCache = new HashMap();
- /**
- * a method signature map based on name and number of methods - the String[]
- * will be the keys into the methodCache A method key is generated by input
- * from some encoded protocol - the method key is object name + method name +
- * parameter number - this returns a full method signature key which is used
- * to look up the method in the methodCache
- */
- final static HashMap> methodOrdinal = new HashMap>();
- final static HashSet objectsCached = new HashSet();
- /**
- * Equivalent to {@link #MIME_TYPE_JSON}
- */
- @Deprecated
- static final String JSON = "application/javascript";
- /**
- * The type that GSON uses when it attempts to deserialize
- * without knowing the target type, e.g. if the target
- * is {@link Object}.
- */
- private static final Class> GSON_DEFAULT_OBJECT_TYPE = LinkedTreeMap.class;
- /**
- * The type that Jackson uses when it attempts to deserialize
- * without knowing the target type, e.g. if the target
- * is {@link Object} and no field matching {@link #CLASS_META_KEY}
- * is found.
- */
- private static final Class> JACKSON_DEFAULT_OBJECT_TYPE = LinkedHashMap.class;
- /**
- * The type that the chosen JSON backend uses when it attempts to deserialize
- * without knowing the target type, e.g. if the target
- * is {@link Object} and no field matching {@link #CLASS_META_KEY}
- * is found.
- */
- public static final Class> JSON_DEFAULT_OBJECT_TYPE = (USING_GSON) ? GSON_DEFAULT_OBJECT_TYPE : JACKSON_DEFAULT_OBJECT_TYPE;
-
- /**
- * Default type for single parameter fromJson(String json), we initially assume this type
- */
- public static final Class> DEFAULT_OBJECT_TYPE = LinkedHashMap.class;
-
-
- /**
- * The {@link Gson} object used for JSON operations when the selected backend is
- * Gson.
- *
- * @see #USING_GSON
- */
- private static final Gson gson = new GsonBuilder().registerTypeAdapterFactory(new GsonPolymorphicTypeAdapterFactory())
- .setDateFormat("yyyy-MM-dd HH:mm:ss.SSS").serializeNulls().disableHtmlEscaping().create();
- /**
- * The {@link Gson} object used to pretty-print JSON.
- */
- private static final Gson prettyGson = new GsonBuilder().registerTypeAdapterFactory(new GsonPolymorphicTypeAdapterFactory())
- .setDateFormat("yyyy-MM-dd HH:mm:ss.SSS").setPrettyPrinting().disableHtmlEscaping().create();
- /**
- * The Jackson {@link ObjectMapper} used for JSON operations when
- * the selected backend is Jackson.
- *
- * @see #USING_GSON
- */
- private static final ObjectMapper mapper = new ObjectMapper();
+ /**
+ * The type that Jackson uses when it attempts to deserialize without knowing
+ * the target type, e.g. if the target is {@link Object} and no field matching
+ * {@link #CLASS_META_KEY} is found.
+ */
+ private static final Class> JACKSON_DEFAULT_OBJECT_TYPE = LinkedHashMap.class;
+ /**
+ * The type that the chosen JSON backend uses when it attempts to deserialize
+ * without knowing the target type, e.g. if the target is {@link Object} and
+ * no field matching {@link #CLASS_META_KEY} is found.
+ */
+ public static final Class> JSON_DEFAULT_OBJECT_TYPE = JACKSON_DEFAULT_OBJECT_TYPE;
+ /**
+ * Default type for single parameter fromJson(String json), we initially
+ * assume this type
+ */
+ public static final Class> DEFAULT_OBJECT_TYPE = LinkedHashMap.class;
- /**
- * The pretty printer to be used with {@link #mapper}
- * when {@link #USING_GSON} equals false, i.e. we're using Jackson.
- */
- private static final PrettyPrinter jacksonPrettyPrinter = new JacksonPrettyPrinter();
+ /**
+ * The Jackson {@link ObjectMapper} used for JSON operations when the selected
+ * backend is Jackson.
+ *
+ */
+ private static final ObjectMapper mapper = new ObjectMapper();
- /**
- * The {@link TypeFactory} used to generate type information for
- * {@link #mapper} when the selected backend is Jackson.
- *
- * No analogue exists for Gson, as it uses a different mechanism
- * to represent types.
- *
- * @see #USING_GSON
- */
- private static final TypeFactory typeFactory = TypeFactory.defaultInstance();
+ /**
+ * The pretty printer to be used with {@link #mapper}
+ */
+ private static final PrettyPrinter jacksonPrettyPrinter = new JacksonPrettyPrinter();
- //Class initializer to setup mapper when the class is loaded
- static {
- //This allows Jackson to work just like GSON when no default constructor is available
- mapper.registerModule(new NoCtorDeserModule());
+ /**
+ * The {@link TypeFactory} used to generate type information for
+ * {@link #mapper} when the selected backend is Jackson.
+ *
+ */
+ private static final TypeFactory typeFactory = TypeFactory.defaultInstance();
- //Actually add our polymorphic support
- mapper.registerModule(JacksonPolymorphicModule.getPolymorphicModule());
+ // Class initializer to setup mapper when the class is loaded
+ static {
+ // This allows Jackson to when no default constructor is
+ // available
+ mapper.registerModule(new NoCtorDeserModule());
- //Disables Jackson's automatic property detection
- mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
- mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
-// mapper.setVisibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.PUBLIC_ONLY);
- mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+ SimpleModule proxySerializerModule = new SimpleModule();
+ proxySerializerModule.addSerializer(Proxy.class, new ProxySerializer());
+ mapper.registerModule(proxySerializerModule);
- //Make jackson behave like gson in that unknown properties are ignored
- mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
- }
+ // Actually add our polymorphic support
+ mapper.registerModule(JacksonPolymorphicModule.getPolymorphicModule());
- /**
- * Ensures a service type name is fully qualified.
- * If the type name is short, it will assume the type
- * exists in the {@code org.myrobotlab.service} package.
- *
- * @param type The service type name, either shortened or
- * fully qualified.
- * @return Null if type is null, otherwise fully qualified name.
- */
- public static String makeFullTypeName(String type) {
- if (type == null) {
- return null;
- }
- if (!type.contains(".")) {
- return String.format("org.myrobotlab.service.%s", type);
- }
- return type;
- }
+ // Disables Jackson's automatic property detection
+ mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
+ mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
+ // mapper.setVisibility(PropertyAccessor.SETTER,
+ // JsonAutoDetect.Visibility.PUBLIC_ONLY);
+ mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
- /**
- * Capitalize the first character of the given string
- *
- * @param line The string to be capitalized
- * @return The capitalized version of line.
- */
- public static String capitalize(final String line) {
- return Character.toUpperCase(line.charAt(0)) + line.substring(1);
- }
+ // Make jackson behave such that unknown properties are ignored
+ mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
- /**
- * Deserializes a JSON string into the target object
- * (or subclass of if {@link #CLASS_META_KEY} exists)
- * using the selected JSON backend.
- *
- * @param json The JSON to be deserialized in String form
- * @param clazz The target class. If a class is not supplied the default class returned will be an {@link #DEFAULT_OBJECT_TYPE}
- * @param The type of the target class.
- * @return An object of the specified class (or a subclass of) with the state
- * given by the json. Null is an allowed return object.
- * @throws JsonDeserializationException if an error during deserialization occurs.
- * @see #USING_GSON
- */
- @SuppressWarnings("unchecked")
- public static /*@Nullable*/ T fromJson(/*@Nonnull*/ String json, /*@Nonnull*/ Class clazz) {
- try {
- if (USING_GSON) {
- if (clazz == null) {
- clazz = (Class)DEFAULT_OBJECT_TYPE;
- }
- return gson.fromJson(json, clazz);
- } else {
- if (clazz == null) {
-
- JsonParser parser = mapper.getFactory().createParser(json);
-
- // "peek" at the next token to determine its type
- JsonToken token = parser.nextToken();
-
- if (token == JsonToken.START_OBJECT) {
- clazz = (Class)Map.class;
- } else if (token == JsonToken.START_ARRAY) {
- clazz = (Class)ArrayList.class;
- } else if (token.isScalarValue()) {
- JsonNode node = mapper.readTree(json);
- if (node.isInt()) {
- return mapper.readValue(json, (Class)Integer.class);
- } else if (node.isBoolean()) {
- return mapper.readValue(json, (Class)Boolean.class);
- } else if (node.isNumber()) {
- return mapper.readValue(json, (Class)Double.class);
- } else if (node.isTextual()) {
- return mapper.readValue(json, (Class)String.class);
- }
- } else {
- log.error("could not derive type from peeking json {}", json);
- }
-
- parser.close();
-
- }
- return mapper.readValue(json, clazz);
- }
- } catch (Exception e) {
- throw new JsonDeserializationException(e);
- }
+ }
+
+ /**
+ * Ensures a service type name is fully qualified. If the type name is short,
+ * it will assume the type exists in the {@code org.myrobotlab.service}
+ * package.
+ *
+ * @param type
+ * The service type name, either shortened or fully qualified.
+ * @return Null if type is null, otherwise fully qualified name.
+ */
+ public static String makeFullTypeName(String type) {
+ if (type == null) {
+ return null;
+ }
+ if (!type.contains(".")) {
+ return ("Service".equals(type)) ? "org.myrobotlab.framework.Service" : String.format("org.myrobotlab.service.%s", type);
}
+ return type;
+ }
- /**
- * Deserializes a JSON string into the target object
- * (or subclass of if {@link #CLASS_META_KEY} exists)
- * using the selected JSON backend.
- *
- * @param json The JSON to be deserialized in String form
- * @param genericClass The target class.
- * @param parameterized The list of types used as the genericClass type parameters
- * of genericClass.
- * @param The type of the target class.
- * @return An object of the specified class (or a subclass of) with the state
- * given by the json. Null is an allowed return object.
- * @throws JsonDeserializationException if an error during deserialization occurs.
- * @see #USING_GSON
- */
- public static /*@Nullable*/ T fromJson(/*@Nonnull*/ String json, /*@Nonnull*/ Class> genericClass,
- /*@Nonnull*/ Class>... parameterized) {
- try {
- if (USING_GSON) {
- return gson.fromJson(json, getType(genericClass, parameterized));
- }
-
- return mapper.readValue(json, typeFactory.constructParametricType(genericClass, parameterized));
- } catch (Exception e) {
- throw new JsonDeserializationException(e);
+ /**
+ * Capitalize the first character of the given string
+ *
+ * @param line
+ * The string to be capitalized
+ * @return The capitalized version of line.
+ */
+ public static String capitalize(final String line) {
+ return Character.toUpperCase(line.charAt(0)) + line.substring(1);
+ }
+
+ /**
+ * Deserializes a JSON string into the target object (or subclass of if
+ * {@link #CLASS_META_KEY} exists) using the selected JSON backend.
+ *
+ * @param json
+ * The JSON to be deserialized in String form
+ * @param clazz
+ * The target class. If a class is not supplied the default class
+ * returned will be an {@link #DEFAULT_OBJECT_TYPE}
+ * @param
+ * The type of the target class.
+ * @return An object of the specified class (or a subclass of) with the state
+ * given by the json. Null is an allowed return object.
+ * @throws JsonDeserializationException
+ * if an error during deserialization occurs.
+ */
+ @SuppressWarnings("unchecked")
+ public static /* @Nullable */ T fromJson(/* @Nonnull */ String json,
+ /* @Nonnull */ Class clazz) {
+ try {
+ if (clazz == null) {
+
+ JsonParser parser = mapper.getFactory().createParser(json);
+
+ // "peek" at the next token to determine its type
+ JsonToken token = parser.nextToken();
+
+ if (token == JsonToken.START_OBJECT) {
+ clazz = (Class) Map.class;
+ } else if (token == JsonToken.START_ARRAY) {
+ clazz = (Class) ArrayList.class;
+ } else if (token.isScalarValue()) {
+ JsonNode node = mapper.readTree(json);
+ if (node.isInt()) {
+ return mapper.readValue(json, (Class) Integer.class);
+ } else if (node.isBoolean()) {
+ return mapper.readValue(json, (Class) Boolean.class);
+ } else if (node.isNumber()) {
+ return mapper.readValue(json, (Class) Double.class);
+ } else if (node.isTextual()) {
+ return mapper.readValue(json, (Class) String.class);
+ }
+ } else {
+ log.error("could not derive type from peeking json {}", json);
}
+
+ parser.close();
+
+ }
+ return mapper.readValue(json, clazz);
+ } catch (Exception e) {
+ throw new JsonDeserializationException(e);
}
+ }
- /**
- * Deserializes a json string into the type represented by
- * {@link T}. {@code type} must must match {@link T} exactly,
- * otherwise the deserializers may not deserialize into T.
- *
- * @param json A string encoded in JSON
- * @param type Reified type information to pass to the deserializers
- * @return An instance of T decoded from the json
- * @param The type to deserialize into
- * @throws JsonDeserializationException if the selected deserializer throws an exception
- */
- public static /*@Nullable*/ T fromJson(/*@NonNull*/ String json, /*@NonNull*/ StaticType type) {
- return fromJson(json, type.getType());
+ /**
+ * Deserializes a JSON string into the target object (or subclass of if
+ * {@link #CLASS_META_KEY} exists) using the selected JSON backend.
+ *
+ * @param json
+ * The JSON to be deserialized in String form
+ * @param genericClass
+ * The target class.
+ * @param parameterized
+ * The list of types used as the genericClass type parameters of
+ * genericClass.
+ * @param
+ * The type of the target class.
+ * @return An object of the specified class (or a subclass of) with the state
+ * given by the json. Null is an allowed return object.
+ * @throws JsonDeserializationException
+ * if an error during deserialization occurs.
+ */
+ public static /* @Nullable */ T fromJson(/* @Nonnull */ String json,
+ /* @Nonnull */ Class> genericClass, /* @Nonnull */ Class>... parameterized) {
+ try {
+ return mapper.readValue(json, typeFactory.constructParametricType(genericClass, parameterized));
+ } catch (Exception e) {
+ throw new JsonDeserializationException(e);
}
+ }
- /**
- * Deserializes a JSON string into the target object
- * (or subclass of if {@link #CLASS_META_KEY} exists)
- * using the selected JSON backend.
- *
- * @param json The JSON to be deserialized in String form
- * @param type The target type.
- * @param The type of the target class.
- * @return An object of the specified class (or a subclass of) with the state
- * given by the json. Null is an allowed return object.
- * @throws JsonDeserializationException if an error during deserialization occurs.
- * @see #USING_GSON
- */
- public static /*@Nullable*/ T fromJson(/*@Nonnull*/ String json, /*@Nonnull*/ Type type) {
- try {
- if (USING_GSON) {
- return gson.fromJson(json, type);
- }
- return mapper.readValue(json, typeFactory.constructType(type));
- } catch (Exception e) {
- throw new JsonDeserializationException(e);
- }
+ /**
+ * Deserializes a json string into the type represented by {@link T}.
+ * {@code type} must must match {@link T} exactly, otherwise the deserializers
+ * may not deserialize into T.
+ *
+ * @param json
+ * A string encoded in JSON
+ * @param type
+ * Reified type information to pass to the deserializers
+ * @return An instance of T decoded from the json
+ * @param
+ * The type to deserialize into
+ * @throws JsonDeserializationException
+ * if the selected deserializer throws an exception
+ */
+ public static /* @Nullable */ T fromJson(/* @NonNull */ String json,
+ /* @NonNull */ StaticType type) {
+ return fromJson(json, type.getType());
+ }
+
+ /**
+ * Deserializes a JSON string into the target object (or subclass of if
+ * {@link #CLASS_META_KEY} exists) using the selected JSON backend.
+ *
+ * @param json
+ * The JSON to be deserialized in String form
+ * @param type
+ * The target type.
+ * @param
+ * The type of the target class.
+ * @return An object of the specified class (or a subclass of) with the state
+ * given by the json. Null is an allowed return object.
+ * @throws JsonDeserializationException
+ * if an error during deserialization occurs.
+ */
+ public static /* @Nullable */ T fromJson(/* @Nonnull */ String json,
+ /* @Nonnull */ Type type) {
+ try {
+ return mapper.readValue(json, typeFactory.constructType(type));
+ } catch (Exception e) {
+ throw new JsonDeserializationException(e);
}
+ }
- /**
- * Convert the given JSON string
- * into an equivalent tree map.
- *
- * @param json The json to be converted
- * @return The json in a tree map form
- * @throws JsonDeserializationException if deserialization fails
- */
- @SuppressWarnings("unchecked")
- public static LinkedTreeMap toTree(String json) {
- try {
- if (USING_GSON) {
- return gson.fromJson(json, LinkedTreeMap.class);
- }
- return (LinkedTreeMap) mapper.readValue(json, LinkedTreeMap.class);
- } catch (Exception e) {
- throw new JsonDeserializationException(e);
- }
+ /**
+ * Convert the given JSON string into an equivalent tree map.
+ *
+ * @param json
+ * The json to be converted
+ * @return The json in a tree map form
+ * @throws JsonDeserializationException
+ * if deserialization fails
+ */
+ @SuppressWarnings("unchecked")
+ public static LinkedHashMap toTree(String json) {
+ try {
+ return (LinkedHashMap) mapper.readValue(json, LinkedHashMap.class);
+ } catch (Exception e) {
+ throw new JsonDeserializationException(e);
}
+ }
+
+ public static Type getType(final Class> rawClass, final Class>... parameterClasses) {
+ return new ParameterizedType() {
+ @Override
+ public Type[] getActualTypeArguments() {
+ return parameterClasses;
+ }
+
+ @Override
+ public Type getRawType() {
+ return rawClass;
+ }
- public static Type getType(final Class> rawClass, final Class>... parameterClasses) {
- return new ParameterizedType() {
- @Override
- public Type[] getActualTypeArguments() {
- return parameterClasses;
- }
+ @Override
+ public Type getOwnerType() {
+ return null;
+ }
- @Override
- public Type getRawType() {
- return rawClass;
- }
+ };
+ }
- @Override
- public Type getOwnerType() {
- return null;
- }
+ static public byte[] getBytes(Object o) throws IOException {
+ ByteArrayOutputStream byteStream = new ByteArrayOutputStream(5000);
+ ObjectOutputStream os = new ObjectOutputStream(new BufferedOutputStream(byteStream));
+ os.flush();
+ os.writeObject(o);
+ os.flush();
+ return byteStream.toByteArray();
+ }
- };
+ /**
+ * Gets the short name of a service. A short name is the name of the service
+ * without any runtime IDs, meaning no '@' signs.
+ *
+ * @param name
+ * The service name to be converted
+ * @return The simple name of the service. If null, will return null, and if
+ * already a simple name then will return name
+ */
+ static public String getShortName(String name) {
+ if (name == null) {
+ return null;
}
+ if (name.contains("@")) {
+ return name.substring(0, name.indexOf("@"));
+ } else {
+ return name;
+ }
+ }
- static public byte[] getBytes(Object o) throws IOException {
- ByteArrayOutputStream byteStream = new ByteArrayOutputStream(5000);
- ObjectOutputStream os = new ObjectOutputStream(new BufferedOutputStream(byteStream));
- os.flush();
- os.writeObject(o);
- os.flush();
- return byteStream.toByteArray();
+ // TODO
+ // public static Object encode(Object, encoding) - dispatches appropriately
+ // should be simple using an enum and map to a new Encoder functional
+ // interface
+
+ /**
+ * Gets the instance id from a service name
+ *
+ * @param name
+ * the name of the instance
+ * @return the name of the instance
+ */
+ static public String getId(String name) {
+ if (name == null) {
+ return null;
+ }
+ if (name.contains("@")) {
+ return name.substring(name.lastIndexOf("@") + 1);
+ } else {
+ return null;
}
+ }
- /**
- * Gets the short name of a service. A
- * short name is the name of the service without
- * any runtime IDs, meaning no '@' signs.
- *
- * @param name The service name to be converted
- * @return The simple name of the service. If null,
- * will return null, and if already a simple name
- * then will return name
- */
- static public String shortName(String name) {
- if (name == null) {
- return null;
- }
- if (name.contains("@")) {
- return name.substring(0, name.indexOf("@"));
- } else {
- return name;
- }
+ /**
+ * Normalizes a service name to its full name, if not already. A full name
+ * consists of two parts: the short name that identifies the service in the
+ * context of its own runtime, and the Runtime ID, that identifies the
+ * service's runtime from others in the network. The two parts are separated
+ * by an {@code @} symbol.
+ *
+ * If this method is given a short name, it is assumed to be local to this
+ * runtime, and it is normalized with the ID of this runtime. If the name is
+ * already a full name, then it is returned unmodified.
+ *
+ * @param name
+ * The service name to normalize
+ * @return The normalized (full) name, or null if name is null
+ */
+ public static String getFullName(String name) {
+ if (name == null) {
+ return null;
}
+ if (getId(name) == null) {
+ return name + '@' + Platform.getLocalInstance().getId();
+ } else {
+ return name;
+ }
+ }
- // TODO
- // public static Object encode(Object, encoding) - dispatches appropriately
- //should be simple using an enum and map to a new Encoder functional interface
+ /**
+ * Checks whether two service names are equal by first normalizing each. If a
+ * name does not have a runtime ID, it is assumed to be a local service.
+ *
+ * @param name1
+ * The first service name
+ * @param name2
+ * The second service name
+ * @return Whether the two names are effectively equal
+ */
+ public static boolean checkServiceNameEquality(String name1, String name2) {
+ return Objects.equals(getFullName(name1), getFullName(name2));
+ }
- /**
- * Gets the instance id from a service name
- *
- * @param name the name of the instance
- * @return the name of the instance
- */
- static public String getId(String name) {
- if (name == null) {
- return null;
- }
- if (name.contains("@")) {
- return name.substring(name.lastIndexOf("@") + 1);
- } else {
- return null;
- }
+
+ /**
+ * Converts a topic method name to the name of the method that is used for
+ * callbacks. Usually this involves prepending the string "on", removing any
+ * "get" or "publish" prefix, and converting it all to proper camelCase.
+ *
+ * @param topicMethod
+ * The topic method name, such as "publishState"
+ * @return The name for the callback method, such as "onState"
+ */
+ static public String getCallbackTopicName(String topicMethod) {
+ // replacements
+ if (topicMethod.startsWith("publish")) {
+ return String.format("on%s", capitalize(topicMethod.substring("publish".length())));
+ } else if (topicMethod.startsWith("get")) {
+ return String.format("on%s", capitalize(topicMethod.substring("get".length())));
}
- /**
- * Converts a topic method name to the name of the method that is
- * used for callbacks. Usually this involves prepending the string
- * "on", removing any "get" or "publish" prefix, and converting
- * it all to proper camelCase.
- *
- * @param topicMethod The topic method name, such as "publishState"
- * @return The name for the callback method, such as "onState"
- */
- static public String getCallbackTopicName(String topicMethod) {
- // replacements
- if (topicMethod.startsWith("publish")) {
- return String.format("on%s", capitalize(topicMethod.substring("publish".length())));
- } else if (topicMethod.startsWith("get")) {
- return String.format("on%s", capitalize(topicMethod.substring("get".length())));
- }
+ // no replacement - just pefix and capitalize
+ // FIXME - subscribe to onMethod --- gets ---> onOnMethod :P
+ return String.format("on%s", capitalize(topicMethod));
+ }
- // no replacement - just pefix and capitalize
- // FIXME - subscribe to onMethod --- gets ---> onOnMethod :P
- return String.format("on%s", capitalize(topicMethod));
+ /**
+ * Gets a String representation of a Message
+ *
+ * @param msg
+ * The message
+ * @return The String representation of the message
+ */
+ static public String getMsgKey(Message msg) {
+ if (msg.sendingMethod != null) {
+ return String.format("%s.%s --> %s.%s(%s) - %d", msg.sender, msg.sendingMethod, msg.name, msg.method, CodecUtils.getParameterSignature(msg.data), msg.msgId);
+ } else {
+ return String.format("%s --> %s.%s(%s) - %d", msg.sender, msg.name, msg.method, CodecUtils.getParameterSignature(msg.data), msg.msgId);
}
+ }
- /**
- * Gets a String representation of a Message
- *
- * @param msg The message
- * @return The String representation of the message
- */
- static public String getMsgKey(Message msg) {
- if (msg.sendingMethod != null) {
- return String.format("%s.%s --> %s.%s(%s) - %d", msg.sender, msg.sendingMethod, msg.name, msg.method, CodecUtils.getParameterSignature(msg.data), msg.msgId);
+ /**
+ * Get a String representing the data in method parameter form, i.e. each
+ * element is separated by a comma. Only {@link #WRAPPER_TYPES} and
+ * {@link MRLListener} will be directly converted to string form using
+ * {@link Object#toString()}, all other types will be represented as their
+ * class's simple name.
+ *
+ * @param data
+ * The list of objects to be represented as a parameter list string.
+ * @return The string representing the data array
+ */
+ static public String getParameterSignature(final Object[] data) {
+ if (data == null) {
+ return "";
+ }
+
+ StringBuffer ret = new StringBuffer();
+ for (int i = 0; i < data.length; ++i) {
+ if (data[i] != null) {
+ Class> c = data[i].getClass(); // not all data types are safe
+ // toString() e.g.
+ // SerializableImage
+ // if (c == String.class || c == Integer.class || c == Boolean.class ||
+ // c == Float.class || c == MRLListener.class) {
+ if (WRAPPER_TYPES.stream().anyMatch(n -> n.equals(c)) || MRLListener.class.equals(c)) {
+ ret.append(data[i].toString());
} else {
- return String.format("%s --> %s.%s(%s) - %d", msg.sender, msg.name, msg.method, CodecUtils.getParameterSignature(msg.data), msg.msgId);
+ String type = data[i].getClass().getCanonicalName();
+ String shortTypeName = type.substring(type.lastIndexOf(".") + 1);
+ ret.append(shortTypeName);
}
- }
- /**
- * Get a String representing the data in method parameter form,
- * i.e. each element is separated by a comma. Only {@link #WRAPPER_TYPES}
- * and {@link MRLListener} will be directly converted to string form using
- * {@link Object#toString()}, all other types will be represented as their class's
- * simple name.
- *
- * @param data The list of objects to be represented as a parameter list string.
- * @return The string representing the data array
- */
- static public String getParameterSignature(final Object[] data) {
- if (data == null) {
- return "";
+ if (data.length != i + 1) {
+ ret.append(",");
}
+ } else {
+ ret.append("null");
+ }
- StringBuffer ret = new StringBuffer();
- for (int i = 0; i < data.length; ++i) {
- if (data[i] != null) {
- Class> c = data[i].getClass(); // not all data types are safe
- // toString() e.g.
- // SerializableImage
- //if (c == String.class || c == Integer.class || c == Boolean.class || c == Float.class || c == MRLListener.class) {
- if (WRAPPER_TYPES.stream().anyMatch(n -> n.equals(c)) || MRLListener.class.equals(c)) {
- ret.append(data[i].toString());
- } else {
- String type = data[i].getClass().getCanonicalName();
- String shortTypeName = type.substring(type.lastIndexOf(".") + 1);
- ret.append(shortTypeName);
- }
-
- if (data.length != i + 1) {
- ret.append(",");
- }
- } else {
- ret.append("null");
- }
+ }
+ return ret.toString();
- }
- return ret.toString();
+ }
+ static public String getServiceType(String inType) {
+ if (inType == null) {
+ return null;
+ }
+ if (inType.contains(".")) {
+ return inType;
}
+ return String.format("org.myrobotlab.service.%s", inType);
+ }
- static public String getServiceType(String inType) {
- if (inType == null) {
- return null;
- }
- if (inType.contains(".")) {
- return inType;
- }
- return String.format("org.myrobotlab.service.%s", inType);
+ /**
+ * Deserializes a message and its data from a JSON string representation into
+ * a fully decoded Message object. This method will first attempt to use the
+ * method cache to determine what types the data elements should be
+ * deserialized to, and if the method cache lookup fails it relies on the
+ * virtual "class" field of the JSON to provide the type information.
+ *
+ * @param jsonData
+ * The serialized Message in JSON form
+ * @return A completely decoded Message object. Null is allowed if the JSON
+ * represented null.
+ * @throws JsonDeserializationException
+ * if jsonData is malformed
+ */
+ public static /* @Nullable */ Message jsonToMessage(/* @Nonnull */ String jsonData) {
+ if (log.isDebugEnabled()) {
+ log.debug("Deserializing message: {}", jsonData);
}
+ Message msg = fromJson(jsonData, Message.class);
- /**
- * Deserializes a message and its data from a JSON
- * string representation into a fully decoded Message
- * object. This method will first attempt to use the
- * method cache to determine what types the data
- * elements should be deserialized to, and if the method
- * cache lookup fails it relies on the virtual "class"
- * field of the JSON to provide the type information.
- *
- * @param jsonData The serialized Message in JSON form
- * @return A completely decoded Message object. Null is allowed if the JSON
- * represented null.
- * @throws JsonDeserializationException if jsonData is malformed
- */
- public static /*@Nullable*/ Message jsonToMessage(/*@Nonnull*/ String jsonData) {
- if (log.isDebugEnabled()) {
- log.debug("Deserializing message: {}",jsonData);
- }
- Message msg = fromJson(jsonData, Message.class);
+ if (msg == null) {
+ log.warn("Null message within json, probably shouldn't happen");
+ return null;
+ }
+ return decodeMessageParams(msg);
+ }
- if (msg == null) {
- log.warn("Null message within json, probably shouldn't happen");
- return null;
+ /**
+ * Performs the second-stage decoding of a Message with JSON-encoded data
+ * parameters. This method is meant to be a helper for the top-level Message
+ * decoding methods to go straight from the various codecs to a completely
+ * decoded Message.
+ *
+ * Package visibility to allow alternative codecs to use this method.
+ *
+ *
+ *
+ * Implementation Details
There are important caveats to note when
+ * using this method as a result of the implementation chosen.
+ *
+ * If the method msg invokes is contained within the {@link MethodCache},
+ * there exists type information for the data parameters and they can be
+ * deserialized into the correct type using this method.
+ *
+ *
+ * However, if no such method exists within the cache this method falls back
+ * on using the embedded virtual meta field ({@link #CLASS_META_KEY}). Since
+ * there is no type information available, there are two main caveats to using
+ * this fallback method:
+ *
+ *
+ *
+ * - Without the type information from the method cache we have no way of
+ * knowing whether to interpret an array as an array of Objects or as a List
+ * (or even what implementor of List to use)
+ *
+ *
+ *
+ * @param msg
+ * The Message object containing the json-encoded data parameters.
+ * This object will be modified in-place
+ * @return A fully-decoded Message
+ * @throws JsonDeserializationException
+ * if any of the data parameters are malformed JSON
+ */
+ public static /* @Nonnull */ Message decodeMessageParams(/* @Nonnull */ Message msg) {
+ String serviceName = msg.getFullName();
+ Class> clazz = Runtime.getClass(serviceName);
+
+ // Nullability of clazz is checked with this, if null
+ // falls back to virt class field
+ boolean useVirtClassField = clazz == null;
+ if (!useVirtClassField) {
+ // For blocking send return type checking
+ ServiceInterface si = Runtime.getService(serviceName);
+ String blockingKey = String.format("%s.%s", msg.getFullName(), msg.getMethod());
+ if (si != null && Message.MSG_TYPE_RETURN.equals(msg.msgType) && si.getInbox().blockingList.containsKey(blockingKey)) {
+ msg.data[0] = fromJson((String) msg.data[0], si.getInbox().blockingList.get(blockingKey).second);
+ msg.encoding = null;
+ return msg;
+ }
+ try {
+ Object[] params = MethodCache.getInstance().getDecodedJsonParameters(clazz, msg.method, msg.data);
+ if (params == null)
+ useVirtClassField = true;
+ else {
+ msg.data = params;
}
- return decodeMessageParams(msg);
+ msg.encoding = null;
+ } catch (RuntimeException e) {
+ log.info(String.format("MethodCache lookup fail: %s.%s", serviceName, msg.method));
+ // Fallback to virtual class field
+ useVirtClassField = true;
+ }
}
- /**
- * Performs the second-stage decoding of a Message
- * with JSON-encoded data parameters. This method is meant
- * to be a helper for the top-level Message decoding methods
- * to go straight from the various codecs to a completely decoded Message.
- *
- * Package visibility to allow alternative codecs to use this method.
- *
- *
- * Implementation Details
- * There are important caveats to note when using
- * this method as a result of the implementation chosen.
- *
- * If the method msg invokes is contained within the
- * {@link MethodCache}, there exists type information
- * for the data parameters and they can be deserialized
- * into the correct type using this method.
- *
- *
- * However, if no such method exists within
- * the cache this method falls back on using the
- * embedded virtual meta field ({@link #CLASS_META_KEY}).
- * Since there is no type information available, there are two
- * main caveats to using this fallback method:
- *
- *
- *
- * - GSON has edge cases for arrays of objects, since
- * we don't know what type is contained within the array we are forced
- * to deserialize it to an array of Objects, but GSON skips our custom
- * deserializer for the Object type. So an array of objects that are not
- * primitives nor Strings *will* deserialize incorrectly unless
- * we are using Jackson.
- * - Without the type information from the method cache we have
- * no way of knowing whether to interpret an array as an array of Objects
- * or as a List (or even what implementor of List to use)
- *
- *
- *
- * @param msg The Message object containing the json-encoded data parameters.
- * This object will be modified in-place
- * @return A fully-decoded Message
- * @throws JsonDeserializationException if any of the data parameters are malformed JSON
- */
- static /*@Nonnull*/ Message decodeMessageParams(/*@Nonnull*/ Message msg) {
- String serviceName = msg.getFullName();
- Class> clazz = Runtime.getClass(serviceName);
- ServiceInterface si = Runtime.getService(serviceName);
-
- //Nullability of clazz is checked with this, if null
- //falls back to virt class field
- boolean useVirtClassField = clazz == null;
- if (!useVirtClassField) {
- String blockingKey = String.format("%s.%s", msg.getFullName(), msg.getMethod());
- if (si != null && Message.MSG_TYPE_RETURN.equals(msg.msgType) && si.getInbox().blockingList.containsKey(blockingKey)) {
- msg.data[0] = fromJson((String) msg.data[0], si.getInbox().blockingList.get(blockingKey).second);
- msg.encoding = null;
- return msg;
- }
- try {
- Object[] params = MethodCache.getInstance().getDecodedJsonParameters(clazz, msg.method, msg.data);
- if (params == null)
- useVirtClassField = true;
- else {
- msg.data = params;
- }
- msg.encoding = null;
- } catch (RuntimeException e) {
- log.info(String.format("MethodCache lookup fail: %s.%s", serviceName, msg.method));
- // Fallback to virtual class field
- useVirtClassField = true;
- }
+ // Not an else since useVirtClassField can be set in the above if block
+ if (useVirtClassField && msg.data != null) {
+ for (int i = 0; i < msg.data.length; i++) {
+ if (msg.data[i] instanceof String) {
+
+ if (isBoolean((String) msg.data[i])) {
+ msg.data[i] = makeBoolean((String) msg.data[i]);
+ } else if (isInteger((String) msg.data[i])) {
+ msg.data[i] = makeInteger((String) msg.data[i]);
+ } else if (isDouble((String) msg.data[i])) {
+ msg.data[i] = makeDouble((String) msg.data[i]);
+ } else if (((String) msg.data[i]).startsWith("\"")) {
+ msg.data[i] = fromJson((String) msg.data[i], String.class);
+ } else if (((String) msg.data[i]).startsWith("[")) {
+ // Array, deserialize to ArrayList to maintain compat with jackson
+ msg.data[i] = fromJson((String) msg.data[i], ArrayList.class);
+ } else {
+ // Object
+ // Serializable should cover everything of interest
+
+ msg.data[i] = fromJson((String) msg.data[i], Serializable.class);
+ }
+
+ if (msg.data[i] != null && JSON_DEFAULT_OBJECT_TYPE.isAssignableFrom(msg.data[i].getClass())) {
+ log.warn("Deserialized parameter to default object type. " + "Possibly missing virtual class field: " + msg.data[i]);
+ }
+ } else {
+ log.error("Attempted fallback Message decoding with virtual class field but " + "parameter is not String: %s");
}
+ }
+ msg.encoding = null;
+ }
- // Not an else since useVirtClassField can be set in the above if block
- if (useVirtClassField) {
- for (int i = 0; i < msg.data.length; i++) {
- if (msg.data[i] instanceof String) {
- // GSON ignores custom deserializers when going to Object
- if (!USING_GSON) {
- msg.data[i] = fromJson((String) msg.data[i], Object.class);
- } else {
- // Workaround because GSON won't deserialize a primitive when
- // given serializable
- if (isBoolean((String) msg.data[i])) {
- msg.data[i] = makeBoolean((String) msg.data[i]);
- } else if(isInteger((String) msg.data[i])) {
- msg.data[i] = makeInteger((String) msg.data[i]);
- } else if (isDouble((String) msg.data[i])) {
- msg.data[i] = makeDouble((String) msg.data[i]);
- } else if (((String) msg.data[i]).startsWith("\"")) {
- msg.data[i] = fromJson((String) msg.data[i], String.class);
- } else if(((String) msg.data[i]).startsWith("[")) {
- // Array, deserialize to ArrayList to maintain compat with jackson
- msg.data[i] = fromJson((String) msg.data[i], ArrayList.class);
- } else {
- // Object
- // Serializable should cover everything of interest
-
- msg.data[i] = fromJson((String) msg.data[i], Serializable.class);
- }
-
- }
-
- if (msg.data[i] != null && JSON_DEFAULT_OBJECT_TYPE.isAssignableFrom(msg.data[i].getClass())) {
- log.warn("Deserialized parameter to default object type. " +
- "Possibly missing virtual class field: " +
- msg.data[i]);
- }
- } else {
- log.error(
- "Attempted fallback Message decoding with virtual class field but " +
- "parameter is not String: %s"
- );
- }
- }
- msg.encoding = null;
- }
+ return msg;
+ }
- return msg;
- }
+ /**
+ * most lossy protocols need conversion of parameters into correctly typed
+ * elements this method is used to query a candidate method to see if a simple
+ * conversion is possible
+ *
+ * @param clazz
+ * the class
+ * @return true/false
+ */
+ public static boolean isSimpleType(Class> clazz) {
+ return WRAPPER_TYPES.contains(clazz) || clazz == String.class;
+ }
- public static Message gsonToMsg(String gsonData) {
- return gson.fromJson(gsonData, Message.class);
- }
+ public static boolean isWrapper(Class> clazz) {
+ return WRAPPER_TYPES.contains(clazz);
+ }
- /**
- * most lossy protocols need conversion of parameters into correctly typed
- * elements this method is used to query a candidate method to see if a simple
- * conversion is possible
- *
- * @param clazz the class
- * @return true/false
- */
- public static boolean isSimpleType(Class> clazz) {
- return WRAPPER_TYPES.contains(clazz) || clazz == String.class;
+ public static boolean isWrapper(String className) {
+ return WRAPPER_TYPES_CANONICAL.contains(className);
+ }
+
+ /**
+ * Converts a snake_case String to a camelCase variant.
+ *
+ * @param s
+ * A String written in snake_case
+ * @return The same String but converted to camelCase
+ */
+ static public String toCamelCase(String s) {
+ String[] parts = s.split("_");
+ String camelCaseString = "";
+ for (String part : parts) {
+ camelCaseString = camelCaseString + toCCase(part);
}
+ return String.format("%s%s", camelCaseString.substring(0, 1).toLowerCase(), camelCaseString.substring(1));
+ }
+
+ /**
+ * Capitalizes the first character of the string while the rest is set to
+ * lower case.
+ *
+ * @param s
+ * The string
+ * @return A String that is all lower case except for the first character
+ */
+ static public String toCCase(String s) {
+ return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
+ }
- public static boolean isWrapper(Class> clazz) {
- return WRAPPER_TYPES.contains(clazz);
+ /**
+ * Convert an Object to its JSON string form using the chosen JSON backend.
+ *
+ * @param o
+ * The object to be converted
+ * @return The object in String JSON form
+ * @throws JsonSerializationException
+ * if serialization fails
+ */
+ public static String toJson(Object o) {
+ try {
+ return mapper.writeValueAsString(o);
+ } catch (Exception e) {
+ throw new JsonSerializationException(e);
}
+ }
- public static boolean isWrapper(String className) {
- return WRAPPER_TYPES_CANONICAL.contains(className);
+ /**
+ * Convert an object to JSON using the chosen backend and write the result to
+ * the specified output stream.
+ *
+ * @param out
+ * The OutputStream that the resultant JSON will be written to.
+ * @param obj
+ * The object that will be converted to JSON.
+ * @throws IOException
+ * if writing to the output stream fails
+ * @throws JsonSerializationException
+ * if an exception occurs during serialization.
+ */
+ static public void toJson(OutputStream out, Object obj) throws IOException {
+ String json;
+ try {
+ json = mapper.writeValueAsString(obj);
+ } catch (Exception jsonProcessingException) {
+ throw new JsonSerializationException(jsonProcessingException);
}
+ if (json != null)
+ out.write(json.getBytes());
+ }
- /**
- * Converts a snake_case String to a camelCase variant.
- *
- * @param s A String written in snake_case
- * @return The same String but converted to camelCase
- */
- static public String toCamelCase(String s) {
- String[] parts = s.split("_");
- String camelCaseString = "";
- for (String part : parts) {
- camelCaseString = camelCaseString + toCCase(part);
- }
- return String.format("%s%s", camelCaseString.substring(0, 1).toLowerCase(), camelCaseString.substring(1));
- }
+ // === method signatures begin ===
- /**
- * Capitalizes the first character of the string while the rest is
- * set to lower case.
- *
- * @param s The string
- * @return A String that is all lower case except for the first character
- */
- static public String toCCase(String s) {
- return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
+ /**
+ * Convert the given object to JSON, as if the object were an instance of the
+ * given class.
+ *
+ * @param o
+ * The object to be serialized
+ * @param clazz
+ * The class to treat the object as
+ * @return The resultant JSON string
+ * @throws JsonSerializationException
+ * if an exception occurs during serialization
+ */
+ public static String toJson(Object o, Class> clazz) {
+ try {
+ return mapper.writerFor(clazz).writeValueAsString(o);
+ } catch (Exception e) {
+ throw new JsonSerializationException(e);
}
+ }
- /**
- * Convert an Object to its JSON string form using the chosen
- * JSON backend.
- *
- * @param o The object to be converted
- * @return The object in String JSON form
- * @throws JsonSerializationException if serialization fails
- * @see #USING_GSON
- */
- public static String toJson(Object o) {
- try {
- if (USING_GSON) {
- return gson.toJson(o);
- }
-
- return mapper.writeValueAsString(o);
- } catch (Exception e) {
- throw new JsonSerializationException(e);
- }
+ /**
+ * Serialize the given object to JSON using the selected JSON backend and
+ * write the result to a file with the given filename.
+ *
+ * @param o
+ * The object to be serialized
+ * @param filename
+ * The name of the file to write the JSON to
+ * @throws IOException
+ * if writing to the file fails
+ * @throws JsonSerializationException
+ * if serialization throws an exception
+ */
+ public static void toJsonFile(Object o, String filename) throws IOException {
+ byte[] json;
+ try {
+ json = mapper.writeValueAsBytes(o);
+ } catch (Exception e) {
+ throw new JsonSerializationException(e);
}
- /**
- * Convert an object to JSON using the chosen backend
- * and write the result to the specified output stream.
- *
- * @param out The OutputStream that the resultant JSON will be
- * written to.
- * @param obj The object that will be converted to JSON.
- * @throws IOException if writing to the output stream fails
- * @throws JsonSerializationException if an exception occurs during serialization.
- * @see #USING_GSON
- */
- static public void toJson(OutputStream out, Object obj) throws IOException {
- String json;
- try {
- if (USING_GSON) {
- json = gson.toJson(obj);
- } else {
- json = mapper.writeValueAsString(obj);
- }
- } catch (Exception jsonProcessingException) {
- throw new JsonSerializationException(jsonProcessingException);
- }
- if (json != null)
- out.write(json.getBytes());
+ // try-wth-resources, ensures a file is closed even if an exception is
+ // thrown
+ try (FileOutputStream fos = new FileOutputStream(filename)) {
+ fos.write(json);
}
+ }
- // === method signatures begin ===
-
- /**
- * Convert the given object to JSON, as if the object were
- * an instance of the given class.
- *
- * @param o The object to be serialized
- * @param clazz The class to treat the object as
- * @return The resultant JSON string
- * @throws JsonSerializationException if an exception occurs during serialization
- * @see #USING_GSON
- */
- public static String toJson(Object o, Class> clazz) {
- try {
- if (USING_GSON) {
- return gson.toJson(o, clazz);
- }
-
- return mapper.writerFor(clazz).writeValueAsString(o);
- } catch (Exception e) {
- throw new JsonSerializationException(e);
- }
- }
+ /**
+ * Converts a given String from camelCase to snake_case, setting the entire
+ * string to be lowercase.
+ *
+ * @param camelCase
+ * The camelCase string to be converted
+ * @return The string in snake_case form
+ */
+ static public String toUnderScore(String camelCase) {
+ return toUnderScore(camelCase, false);
+ }
- /**
- * Serialize the given object to JSON using the selected
- * JSON backend and write the result to a file with the
- * given filename.
- *
- * @param o The object to be serialized
- * @param filename The name of the file to write the JSON to
- * @throws IOException if writing to the file fails
- * @throws JsonSerializationException if serialization throws an exception
- */
- public static void toJsonFile(Object o, String filename) throws IOException {
- byte[] json;
- try {
- if (USING_GSON) {
- json = gson.toJson(o).getBytes();
- } else {
- json = mapper.writeValueAsBytes(o);
- }
- } catch (Exception e) {
- throw new JsonSerializationException(e);
+ /**
+ * Converts a given String from camelCase to snake_case, If toLowerCase is
+ * true, the entire string will be set to lower case. If false, it will be set
+ * to uppercase, and if null the casing will not be changed.
+ *
+ * @param camelCase
+ * The camelCase string to be converted
+ * @param toLowerCase
+ * Whether the entire string should be lowercase, uppercase, or not
+ * changed (null)
+ * @return The string in snake_case form
+ */
+ static public String toUnderScore(String camelCase, Boolean toLowerCase) {
+
+ byte[] a = camelCase.getBytes();
+ boolean lastLetterLower = false;
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < a.length; ++i) {
+ boolean currentCaseUpper = Character.isUpperCase(a[i]);
+
+ Character newChar = null;
+ if (toLowerCase != null) {
+ if (toLowerCase) {
+ newChar = (char) Character.toLowerCase(a[i]);
+ } else {
+ newChar = (char) Character.toUpperCase(a[i]);
}
+ } else {
+ newChar = (char) a[i];
+ }
- //try-wth-resources, ensures a file is closed even if an exception is thrown
- try (FileOutputStream fos = new FileOutputStream(filename)) {
- fos.write(json);
- }
+ sb.append(String.format("%s%c", (lastLetterLower && currentCaseUpper) ? "_" : "", newChar));
+ lastLetterLower = !currentCaseUpper;
}
- /**
- * Converts a given String from camelCase to snake_case,
- * setting the entire string to be lowercase.
- *
- * @param camelCase The camelCase string to be converted
- * @return The string in snake_case form
- */
- static public String toUnderScore(String camelCase) {
- return toUnderScore(camelCase, false);
- }
+ return sb.toString();
- /**
- * Converts a given String from camelCase to snake_case,
- * If toLowerCase is true, the entire string will be set to
- * lower case. If false, it will be set to uppercase, and if
- * null the casing will not be changed.
- *
- * @param camelCase The camelCase string to be converted
- * @param toLowerCase Whether the entire string should be lowercase, uppercase,
- * or not changed (null)
- * @return The string in snake_case form
- */
- static public String toUnderScore(String camelCase, Boolean toLowerCase) {
-
- byte[] a = camelCase.getBytes();
- boolean lastLetterLower = false;
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < a.length; ++i) {
- boolean currentCaseUpper = Character.isUpperCase(a[i]);
-
- Character newChar = null;
- if (toLowerCase != null) {
- if (toLowerCase) {
- newChar = (char) Character.toLowerCase(a[i]);
- } else {
- newChar = (char) Character.toUpperCase(a[i]);
- }
- } else {
- newChar = (char) a[i];
- }
-
- sb.append(String.format("%s%c", (lastLetterLower && currentCaseUpper) ? "_" : "", newChar));
- lastLetterLower = !currentCaseUpper;
- }
+ }
- return sb.toString();
+ /**
+ * Equivalent to {@link #isInteger(String)}
+ *
+ * @param string
+ * The String to be checked
+ * @return Whether the String can be parsed as an Integer
+ */
+ @Deprecated
+ public static boolean tryParseInt(String string) {
+ try {
+ Integer.parseInt(string);
+ return true;
+ } catch (Exception e) {
}
+ return false;
+ }
- /**
- * Equivalent to {@link #isInteger(String)}
- *
- * @param string The String to be checked
- * @return Whether the String can be parsed as an Integer
- */
- @Deprecated
- public static boolean tryParseInt(String string) {
- try {
- Integer.parseInt(string);
- return true;
- } catch (Exception e) {
-
- }
- return false;
+ public static String type(String type) {
+ int pos0 = type.indexOf(".");
+ if (pos0 > 0) {
+ return type;
}
+ return String.format("org.myrobotlab.service.%s", type);
+ }
- public static String type(String type) {
- int pos0 = type.indexOf(".");
- if (pos0 > 0) {
- return type;
- }
- return String.format("org.myrobotlab.service.%s", type);
+ /**
+ * Get the simple name of a service type name. A simple name is the name of
+ * the service type without any package specifier.
+ *
+ * @param serviceType
+ * The service type in String form
+ * @return The simple name of the servide type
+ */
+ public static String getSimpleName(String serviceType) {
+ int pos = serviceType.lastIndexOf(".");
+ if (pos > -1) {
+ return serviceType.substring(pos + 1);
}
+ return serviceType;
+ }
- /**
- * Get the simple name of a service type name.
- * A simple name is the name of the service type
- * without any package specifier.
- *
- * @param serviceType The service type in String form
- * @return The simple name of the servide type
- */
- public static String getSimpleName(String serviceType) {
- int pos = serviceType.lastIndexOf(".");
- if (pos > -1) {
- return serviceType.substring(pos + 1);
- }
- return serviceType;
- }
+ public static String getSafeReferenceName(String name) {
+ return name.replaceAll("[@/ .-]", "_");
+ }
- public static String getSafeReferenceName(String name) {
- return name.replaceAll("[@/ .-]", "_");
+ /**
+ * Serializes the specified object to JSON, using
+ * {@link #mapper} with {@link #jacksonPrettyPrinter} to pretty-ify the
+ * result.
+ *
+ * @param ret
+ * The object to be serialized
+ * @return The object in pretty JSON form
+ */
+ public static String toPrettyJson(Object ret) {
+ try {
+ return mapper.writer(jacksonPrettyPrinter).writeValueAsString(ret);
+ } catch (Exception e) {
+ throw new JsonSerializationException(e);
}
- /**
- * Serializes the specified object to JSON, using
- * {@link #prettyGson} or {@link #mapper}
- * with {@link #jacksonPrettyPrinter}
- * to pretty-ify the result. Which object is used depends on
- * {@link #USING_GSON}
- *
- * @param ret The object to be serialized
- * @return The object in pretty JSON form
- */
- public static String toPrettyJson(Object ret) {
- try {
- if (USING_GSON) {
- return prettyGson.toJson(ret);
- } else {
-
- return mapper.writer(jacksonPrettyPrinter).writeValueAsString(ret);
- }
- } catch (Exception e) {
- throw new JsonSerializationException(e);
- }
+ }
+ /**
+ * Deserialize a given String into an array of Objects, treating the String as
+ * JSON and using the selected JSON backend.
+ *
+ * @param data
+ * A String containing a JSON array
+ * @return An array of Objects created by deserializing the JSON array
+ * @throws Exception
+ * If deserialization fails
+ */
+ static public Object[] decodeArray(Object data) throws Exception {
+ // ITS GOT TO BE STRING - it just has to be !!! :)
+ String instr = (String) data;
+ // array of Strings ? - don't want to double encode !
+ Object[] ret = null;
+ synchronized (data) {
+ ret = mapper.readValue(instr, Object[].class);
}
+ return ret;
+ }
- /**
- * Deserialize a given String into an array of Objects,
- * treating the String as JSON and using the selected JSON backend.
- *
- * @param data A String containing a JSON array
- * @return An array of Objects created by deserializing the JSON array
- * @throws Exception If deserialization fails
- */
- static public Object[] decodeArray(Object data) throws Exception {
- // ITS GOT TO BE STRING - it just has to be !!! :)
- String instr = (String) data;
- // array of Strings ? - don't want to double encode !
- Object[] ret = null;
- synchronized (data) {
- if (USING_GSON) {
- ret = gson.fromJson(instr, Object[].class);
- } else {
- ret = mapper.readValue(instr, Object[].class);
- }
- }
- return ret;
- }
+ /**
+ * This is a Path to Message decoder - it takes a line of text and generates
+ * the appropriate msg with json encoded string parameters and either invokes
+ * (locally) or sendBlockingRemote (remotely)
+ *
+ *
+ *
+ * The expectation of this encoding is:
+ * if "/api/service/" is found - the end of that string is the starting point
+ * if "/api/service/" is not found - then the starting point of the string should be the service
+ * e.g "runtime/getUptime"
+ *
+ * Important to remember getRequestURI is NOT decoded and getPathInfo is.
+ *
+ *
+ *
+ * Method URL-Decoded Result
+ * ----------------------------------------------------
+ * getContextPath() no /app
+ * getLocalAddr() 127.0.0.1
+ * getLocalName() 30thh.loc
+ * getLocalPort() 8480
+ * getMethod() GET
+ * getPathInfo() yes /a?+b
+ * getProtocol() HTTP/1.1
+ * getQueryString() no p+1=c+dp+2=e+f
+ * getRequestedSessionId() no S%3F+ID
+ * getRequestURI() no /app/test%3F/a%3F+b;jsessionid=S+ID
+ * getRequestURL() no http://30thh.loc:8480/app/test%3F/a%3F+b;jsessionid=S+ID
+ * getScheme() http
+ * getServerName() 30thh.loc
+ * getServerPort() 8480
+ * getServletPath() yes /test?
+ * getParameterNames() yes [p 2, p 1]
+ * getParameter("p 1") yes c d
+ *
+ *
+ * @param from
+ * - sender
+ * @param path
+ * - cli encoded msg
+ * @return - a Message derived from cli
+ */
+ static public Message pathToMsg(String from, String path) {
+ // Message msg = Message.createMessage(from,"ls", null);
+ Message msg = new Message();
+ msg.name = "runtime"; // default ?
+ msg.method = "ls";
+
+ // not required unless asynchronous
+ msg.sender = from;
/**
- * This is the Cli encoder - it takes a line of text and generates the
- * appropriate msg from it to either invoke (locally) or sendBlockingRemote
- * (remotely)
- *
*
- *
- * The expectation of this encoding is:
- * if "/api/service/" is found - the end of that string is the starting point
- * if "/api/service/" is not found - then the starting point of the string should be the service
- * e.g "runtime/getUptime"
- *
- * Important to remember getRequestURI is NOT decoded and getPathInfo is.
- *
+
+ The key to this interface is leading "/" ...
+ "/" is absolute path - dir or execute
+ without "/" means runtime method - spaces and quotes can be delimiters
+
+ "/" - list services
+ "/{serviceName}" - list data of service
+ "/{serviceName}/" - list methods of service
+ "/{serviceName}/{method}" - invoke method
+ "/{serviceName}/{method}/" - list parameters of method
+ "/{serviceName}/{method}/jsonP0/jsonP1/jsonP2/..." - invoke method with parameters
+
+ or runtime
+ {method}
+ {method}/
+ {method}/p01
*
*
- * Method URL-Decoded Result
- * ----------------------------------------------------
- * getContextPath() no /app
- * getLocalAddr() 127.0.0.1
- * getLocalName() 30thh.loc
- * getLocalPort() 8480
- * getMethod() GET
- * getPathInfo() yes /a?+b
- * getProtocol() HTTP/1.1
- * getQueryString() no p+1=c+dp+2=e+f
- * getRequestedSessionId() no S%3F+ID
- * getRequestURI() no /app/test%3F/a%3F+b;jsessionid=S+ID
- * getRequestURL() no http://30thh.loc:8480/app/test%3F/a%3F+b;jsessionid=S+ID
- * getScheme() http
- * getServerName() 30thh.loc
- * getServerPort() 8480
- * getServletPath() yes /test?
- * getParameterNames() yes [p 2, p 1]
- * getParameter("p 1") yes c d
*
- *
- * @param contextPath - prefix to be added if supplied
- * @param from - sender
- * @param to - target service
- * @param cmd - cli encoded msg
- * @return - a Message derived from cli
*/
- static public Message cliToMsg(String contextPath, String from, String to, String cmd) {
- Message msg = Message.createMessage(from, to, "ls", null);
-
- /**
- *
-
- The key to this interface is leading "/" ...
- "/" is absolute path - dir or execute
- without "/" means runtime method - spaces and quotes can be delimiters
-
- "/" - list services
- "/{serviceName}" - list data of service
- "/{serviceName}/" - list methods of service
- "/{serviceName}/{method}" - invoke method
- "/{serviceName}/{method}/" - list parameters of method
- "/{serviceName}/{method}/p0/p1/p2" - invoke method with parameters
-
- or runtime
- {method}
- {method}/
- {method}/p01
- *
- *
- *
- */
-
- cmd = cmd.trim();
-
- // remove uninteresting api prefix
- if (cmd.startsWith(API_SERVICE_PATH)) {
- cmd = cmd.substring(API_SERVICE_PATH.length());
- }
-
- if (contextPath != null) {
- cmd = contextPath + cmd;
- }
- // assume runtime as 'default'
- if (msg.name == null) {
- // FIXME "runtime" really needs to be a constant at the very least
- msg.name = "runtime";
- }
+ path = path.trim();
- // two possibilities - either it begins with "/" or it does not
- // if it does begin with "/" its an absolute path to a dir, ls, or invoke
- // if not then its a runtime method
-
- if (cmd.startsWith("/")) {
- // ABSOLUTE PATH !!!
- String[] parts = cmd.split("/");
-
- if (parts.length < 3) {
- msg.method = "ls";
- msg.data = new Object[]{cmd};
- return msg;
- }
-
- // fix me diff from 2 & 3 "/"
- if (parts.length >= 3) {
- // prepare to parse the arguments
-
- msg.name = parts[1];
- // prepare the method
- msg.method = parts[2].trim();
-
- // FIXME - to encode or not to encode that is the question ...
- // This source comes from the cli - which is "all" strings
- // in theory it needs to be decoded from an all strings interface
- // json is an all string interface so we will decode from cli strings
- // (not json)
- // using a json decoder - cuz it will work :P - and string will decode
- // to a string
- Object[] payload = new Object[parts.length - 3];
- for (int i = 3; i < parts.length; ++i) {
- if (isInteger(parts[i])) {
- payload[i - 3] = makeInteger(parts[i]);
- } else if (isDouble(parts[i])) {
- payload[i - 3] = makeDouble(parts[i]);
- } else if (parts[i].equals("true") || parts[i].equals("false")) {
- payload[i - 3] = makeBoolean(parts[i]);
- } else { // String
- // sloppy as the cli does not require quotes \" but json does
- // humans won't add quotes - but we will
- payload[i - 3] = parts[i];
- }
- }
-
- msg.data = payload;
- }
- return msg;
- } else {
- // NOT ABOSLUTE PATH - SIMILAR TO EXECUTING IN THE RUNTIME /usr/bin path
- // (ie runtime methods!)
- // spaces for parameter delimiters ?
- String[] spaces = cmd.split(" ");
- // FIXME - need to deal with double quotes e.g. func A "B and C" D - p0 =
- // "A" p1 = "B and C" p3 = "D"
- msg.method = spaces[0];
- Object[] payload = new Object[spaces.length - 1];
- for (int i = 1; i < spaces.length; ++i) {
- // webgui will never use this section of code
- // currently the codepath is only excercised by InProcessCli
- // all of this methods will be "optimized" single commands to runtime (i
- // think)
- // so we are going to error on the side of String parameters - other
- // data types will have problems
- payload[i - 1] = spaces[i];
- }
- msg.data = payload;
-
- return msg;
- }
+ // remove uninteresting api prefix
+ if (path.startsWith(API_SERVICE_PATH)) {
+ path = path.substring(API_SERVICE_PATH.length());
}
- /**
- * Parse the specified data as an Integer. If parsing
- * fails, returns null
- *
- * @param data The String to be coerced into an Integer
- * @return the data as an Integer, if parsing fails then null instead
- */
- static public Integer makeInteger(String data) {
- try {
- return Integer.parseInt(data);
- } catch (Exception e) {
- }
- return null;
- }
+ // two possibilities - either it begins with "/" or it does not
+ // if it does begin with "/" its an absolute path to a dir, ls, or invoke
+ // if not then its a runtime method
+
+ if (path.startsWith("/")) {
+ // ABSOLUTE PATH !!!
+ String[] parts = path.split("/"); // <- this breaks things ! e.g.
+ // /runtime/connect/"http://localhost:8888"
+ // path parts less than 3 is a dir or ls
+ if (parts.length < 3) {
+ // this morphs a path which has less than 3 parts
+ // into a runtime "ls" method call to do reflection of services or service methods
+ // e.g. /clock -> /runtime/ls/"/clock"
+ // e.g. /clock/ -> /runtime/ls/"/clock/"
+
+ msg.method = "ls";
+ msg.data = new Object[] { "\"" + path + "\""};
+ return msg;
+ }
- /**
- * Checks whether the given String can be parsed as
- * an Integer
- *
- * @param data The string to be checked
- * @return true if the data can be parsed as an Integer, false otherwise
- */
- static public boolean isInteger(String data) {
- try {
- Integer.parseInt(data);
- return true;
- } catch (Exception e) {
- }
- return false;
- }
+ // ["", "runtime", "shutdown"]
+ if (parts.length == 3) {
+ msg.name = parts[1];
+ msg.method = parts[2];
+ return msg;
+ }
- /**
- * Checks whether the given String can be parsed as
- * a Double
- *
- * @param data The string to be checked
- * @return true if the data can be parsed as a Double, false otherwise
- */
- static public boolean isDouble(String data) {
- try {
- Double.parseDouble(data);
- return true;
- } catch (Exception e) {
- }
- return false;
+ // fix me diff from 2 & 3 "/"
+ if (parts.length >= 3) {
+ // prepare to parse the arguments
+
+ msg.name = parts[1];
+ // prepare the method
+ msg.method = parts[2].trim();
+
+ // remove the first 3 slashes
+ String data = path.substring(("/" + msg.name + "/" + msg.method + "/").length());
+ msg.data = extractJsonParamsFromPath(data);
+ }
+ return msg;
+ } else {
+ // e.g. ls /webgui/
+ // retrieves all webgui methods
+ String[] spaces = path.split(" ");
+ // FIXME - need to deal with double quotes e.g. func A "B and C" D - p0 =
+ // "A" p1 = "B and C" p3 = "D"
+ msg.method = spaces[0];
+ Object[] payload = new Object[spaces.length - 1];
+ for (int i = 1; i < spaces.length; ++i) {
+ // webgui will never use this section of code
+ // currently the codepath is only excercised by InProcessCli
+ // all of this methods will be "optimized" single commands to runtime (i
+ // think)
+ // so we are going to error on the side of String parameters - other
+ // data types will have problems
+ payload[i - 1] = spaces[i];
+ }
+ msg.data = payload;
+
+ return msg;
}
+ }
- /**
- * Parse the specified data as a Double. If parsing
- * fails, returns null
- *
- * @param data The String to be coerced into a Doubled=
- * @return the data as a Double, if parsing fails then null instead
- */
- static public Double makeDouble(String data) {
- try {
- return Double.parseDouble(data);
- } catch (Exception e) {
+ /**
+ * extractJsonFromPath exects a forwad slash deliminated string
+ *
+ *
+ * json1 / json2 / json3
+ *
+ *
+ * It will return the json parts in a string array
+ *
+ * @param input
+ * @return
+ */
+ public static Object[] extractJsonParamsFromPath(String input) {
+ List